Applies to versions earlier than 2026.5.x. From v2026.5.2, if your PDF is rendered from HTML, IronPDF can create bookmarks from headings automatically: set RenderingOptions.AutoBookmarksFromHeadings = true. For the most reliable page mapping, also set AutoBookmarkCssSelectors = new[] { "h1", "h2", "h3" }. To add clickable links to specific pages and positions, use the LinkAnnotation class.

The rest of this article covers the manual approach for earlier versions.

Garbled Text from Pages.Lines When Adding Bookmarks

Curtis Chau
Curtis Chau
Updated: October 29, 2026

Adding bookmarks based on text content sometimes fails because Pages.Lines returns garbled or unordered text, which breaks any search for a section title. This happens most often when the PDF was generated from HTML or a third-party tool with a complex layout.

Pages.Lines reconstructs text from how it visually appears on the page, so it can produce incomplete or mixed-up results when:

  • Text is drawn in non-linear order.
  • Custom fonts or encodings are in use.
  • The PDF comes from HTML or third-party tools with complex layouts.

Any of these makes searching for a specific phrase or section header unreliable.

Solution

Switch to ExtractTextFromPage(). It pulls the raw embedded text from each page in logical reading order, which gives a cleaner, more consistent result for locating text when generating bookmarks dynamically.

1. Map section headers to bookmark titles

Suppose you have a long generated PDF with sections titled "ARTICLE 1", "ARTICLE 2", and so on, and you want a bookmark pointing to each one. Build a dictionary from the search key to the full bookmark title, then walk every page and bookmark the first page where each key appears.

var pdf = PdfDocument.FromFile("contract.pdf");
// Map section headers to bookmark titles
var articles = new Dictionary<string, string>
{
    { "ARTICLE 1", "ARTICLE 1 - THE SUBCONTRACT DOCUMENTS" },
    { "ARTICLE 2", "ARTICLE 2 - MUTUAL RIGHTS AND RESPONSIBILITIES" },
    { "ARTICLE 3", "ARTICLE 3 - CONTRACTOR" },
    { "ARTICLE 4", "ARTICLE 4 - SUBCONTRACTOR" },
    { "ARTICLE 5", "ARTICLE 5 - CHANGES IN THE WORK" },
    { "ARTICLE 6", "ARTICLE 6 - CLAIMS AND DISPUTES" },
    { "ARTICLE 7", "ARTICLE 7 - TERMINATION, SUSPENSION OR ASSIGNMENT OF THE SUBCONTRACT" },
    { "ARTICLE 8", "ARTICLE 8 - THE WORK OF THIS SUBCONTRACT" },
    { "ARTICLE 9", "ARTICLE 9 - DATE OF COMMENCEMENT AND SUBSTANTIAL COMPLETION" },
    { "ARTICLE 10", "ARTICLE 10 - SUBCONTRACT SUM" },
    { "ARTICLE 11", "ARTICLE 11 - PAYMENTS" },
    { "ARTICLE 12", "ARTICLE 12 - INSURANCE AND BONDS" },
    { "ARTICLE 13", "ARTICLE 13 - TEMPORARY FACILITIES, SERVICES, EQUIPMENT, AND WORKING CONDITIONS" },
    { "ARTICLE 14", "ARTICLE 14 - MISCELLANEOUS PROVISIONS" },
    { "ARTICLE 15", "ARTICLE 15 - ENUMERATION OF SUBCONTRACT DOCUMENTS" }
};
// Track which articles have already been bookmarked
var bookmarkedArticles = new HashSet<string>();
for (int i = 0; i < pdf.Pages.Count; i++)
{
    string pageText = pdf.ExtractTextFromPage(i);
    foreach (var article in articles)
    {
        if (!bookmarkedArticles.Contains(article.Key) && pageText.Contains(article.Key))
        {
            pdf.Bookmarks.AddBookMarkAtEnd(article.Value, i);
            bookmarkedArticles.Add(article.Key);
        }
    }
}
pdf.SaveAs("bookmarkadded.pdf");
C#

AddBookMarkAtEnd() appends the full title and points it at page index i. The HashSet<string> guards against duplicates, so each article is bookmarked only on the first page where it is found.

2. Why this holds up

  • ExtractTextFromPage() gives reliable access to plain text across varying PDF structures.
  • Looping through every page means no section is missed, wherever it appears.
  • The HashSet<string> keeps each article to a single bookmark.

The resulting PDF carries a bookmark for each specified article heading, even when the source was dynamic HTML or had complex formatting.

Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.

Read More

Ready to Get Started?

Nuget Downloads 22,024,882Version:2026.11just released