Technical Article

Structure-Order PDF Text Extraction in Delphi With HotPDF

Every geometric text extractor is guessing. It reads the glyphs a page draws, sorts them by baseline and horizontal position, and hopes the visual arrangement matches the order a human would read. On a single-column report that guess is right. On a two-column journal article, a form with a sidebar, or a table whose cells were emitted column by column, it is wrong in ways that are hard to notice and expensive to discover downstream. HotPDF answers this with ExtractLoadedPageStructureText, which ignores geometry entirely: it walks the document structure tree in authoring order as defined by ISO 32000-1 §14.8.4, then reassembles the page glyphs by their marked-content identifier. For a tagged PDF that is not a heuristic, it is the order the producing application declared

The function returns False when the page has no usable structure tree, which is the signal to fall back to the geometric extractor rather than to fail. That two-path design matters more than the algorithm: real document intake sees tagged government forms and scanner output in the same folder, and a pipeline that only handles one of them is not a pipeline

Why does geometric extraction get the reading order wrong?

Because a PDF content stream carries no reading order at all. It is a sequence of drawing operators, and a producer is free to emit them in whatever sequence suits its own layout engine. Word processors usually emit in flow order and geometric sorting looks fine. Layout tools, form designers, and report generators frequently do not: a page footer can be emitted before the body, a table can be filled column-major, and a two-column page can interleave lines from both columns because the composer resolved them together

Two-column PDF page comparison showing baseline-sorted geometric extraction splicing columns against structure-order MCID extraction in HotPDF
Sorting glyphs by baseline interleaves two columns into nonsense, while the structure tree replays the order the producer declared

The failure mode is quiet. A geometric extractor never reports an error, it just hands back prose whose sentences are spliced from two columns. Anything consuming that text, a search index, an e-invoice field mapper, a retrieval pipeline feeding a language model, inherits the damage without a warning. HotPDF also ships the geometric extractors for loaded documents, and they remain the right tool for untagged files; the point of the structure-order path is to stop guessing when the document already carries the answer

What the structure tree actually stores

A tagged PDF holds a second, parallel description of the page. The catalog points at a /StructTreeRoot, whose /K children form a tree of structure elements: /Document, /Sect, /P, /Table, /TR, /TD, and so on. The leaves of that tree are marked-content references, integers that name a span of the page content stream. On the content side, those spans are opened with a BDC operator carrying an /MCID and closed with EMC. Each structure element also carries a /Pg entry naming the page it belongs to, which is what makes per-page traversal possible in a document whose structure tree spans hundreds of pages

PDF structure tree anatomy linking StructTreeRoot elements like Sect, Table, TR and TD to BDC MCID spans in the HotPDF page content stream
Leaves of the tree are marked-content references, and each element carries a Pg entry that lets the walk filter to the current page

HotPDF traverses that tree with a depth cap of 128 levels and filters on /Pg so only the current page contributes. The output of the traversal is not text, it is an ordered list of MCID values: the authoring order of the marked-content spans on this page. Reassembling text is then a matter of replaying the glyphs in that order

The MCID is recorded during glyph extraction, not looked up afterwards

This is the implementation detail that makes the feature cheap. HotPDF already records the active marked-content identifier on every glyph it extracts, in the MCID field of THPDFGlyphRecord, because the content-stream interpreter knows which BDC scope is open at the moment it processes each Tj or TJ operator. Structure-order extraction therefore needs no second pass over the content stream. It collects the MCID sequence from the structure tree, then buckets the already-extracted glyphs by MCID and emits them in that sequence

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // caller-owned diagnostics sink
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Authoring order straight from the structure tree
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // No usable structure tree on this page: geometric fallback
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Untagged glyphs are counted, never silently dropped

A page can be partly tagged. Producers add a decorative rule, a page number, or a late-stage watermark outside any BDC scope, and those glyphs belong to no MCID. Dropping them would be the tidy implementation and the wrong one, because the same gap also appears when a producer tags the body but forgets the table, and you would lose the table without noticing

HotPDF appends unclaimed glyphs as a geometric tail after the structure-ordered text and reports their number through the UntaggedGlyphCount output parameter. That number is a quality signal you can act on. A handful of glyphs on a page of two thousand is page furniture and can be ignored. Forty percent of the page outside the structure tree means the tagging is decorative and the geometric extractor is the more honest answer for that file

Decision flow for HotPDF structure text extraction with geometric fallback when a page has no usable structure tree or decorative tagging
True means structure order with the untagged tail appended, and False routes the page to the geometric extractor instead of failing
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Trust the structure tree only when it claims most of the page
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

What makes the function return False

Three cases, and they are worth distinguishing because only one of them is a defect in the document. The first is an ordinary untagged PDF: no /StructTreeRoot, nothing to walk, and False is simply the truth. The second is a scanned page whose text comes from an OCR layer that was never tagged. The third is the interesting one: content that carries BDC operators with /MCID values but whose page has no /StructParents entry and whose structure tree never references those identifiers. The marked content exists, the structure side does not, and there is no order to recover. HotPDF reports False rather than inventing one

That last case shows up in hand-edited files and in output from tools that emit marked content for optional-content or artifact purposes without building a structure tree. If you are producing tagged PDFs yourself, the same asymmetry is what PDF/UA validation checks for, and the writer-side counterpart is covered in the layout DOM that emits tagged, paginated output

Where structure order pays for itself

Accessibility auditing is the obvious one: if you are certifying a document against PDF/UA, the reading order a screen reader will announce is exactly the structure order, so extracting it is how you review it without a screen reader. Data capture is the larger commercial case. Tagged government forms, regulated disclosures, and e-invoice attachments carry field labels and values in declared order, and reading them in that order removes an entire class of mapping bugs that geometric extraction creates on multi-column layouts

The newest consumer is retrieval for language models. Chunking a document for embedding is only as good as the text order, and a chunk that splices two columns produces sentences that never existed. Structure-order extraction is the cheapest available fix for that, because for tagged documents the correct order is already in the file and just needs to be read

HotPDF is a native VCL component for Delphi and C++Builder, so the structure-tree traversal and the glyph replay both run in-process against a loaded document with no external renderer involved. Full API details for the loaded-document extraction family are on the HotPDF Delphi PDF component product page