Open a generated report in a screen reader and the usual failures show up within a minute: "Page 3 of 40" is read aloud on every page, the company logo is announced as "graphic" with nothing after it, and a table comes out as one long run of numbers with no column names. PDF Library for Delphi avoids all three with automatic structure tagging: call SetAutoTagMode once, and ordinary drawing calls produce paragraphs, headings, figures, tables and skippable page furniture as the document is drawn
The manual route still exists. You can wrap every drawing call in BeginTag and EndTag, and for documents with unusual structure that is the right tool. For an ordinary report, invoice or statement, though, hand tagging makes accessibility depend on nobody ever forgetting a pair, in every code path that draws anything, for the life of the product. We would not bet a compliance requirement on that
What SetAutoTagMode switches on
SetAutoTagMode takes a bit mask and returns the mode that was in force before the call. Each bit covers one kind of content:
AUTOTAG_TEXT(1): text becomes a paragraph, or a heading when one has been registeredAUTOTAG_FURNITURE(2): running headers, footers and page numbers become artifacts that a reader skipsAUTOTAG_FIGURE(4): a drawn image becomes a figure, or an artifact when you declared it decorativeAUTOTAG_TABLE(8): drawn tables carry their rows and cells into the structure treeAUTOTAG_DEFAULT(15): all four, and the value we recommend for any new report code
Turning the mode on also marks the document as tagged, and that step matters more than it looks. Under ISO 32000-1 §14.7.1 a reader treats a file as untagged unless the catalog's /MarkInfo dictionary says /Marked true. A file can carry a complete, correct structure tree and still be announced as having no structure at all, because the one flag that tells assistive technology to look for it is missing. The tree is there. Nothing reads it
Text tagging works per call. Each DrawText, DrawWrappedText or DrawTextBox call becomes one element, so a paragraph drawn line by line with five DrawText calls reaches the reader as five one-line paragraphs. If your layout code already breaks lines itself, that is worth changing: hand the whole paragraph to DrawWrappedText and let one call produce one paragraph
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // top-left origin, y grows downward
Lib.SetDocumentLanguage('en-US');
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // before any RegisterHeading call
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // tagged H1
Lib.SetTextSize(11);
Lib.DrawWrappedText(72, 130, 450,
'Every unit installed before 2024 was inspected. ' +
'Units that failed the torque check were serviced on site.'); // one P
// ... remaining pages ...
// Runs over every existing page, so call it once the pages are drawn
Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1); // Pagination artifact
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
The comment on the third line is there for a reason. RegisterHeading only queues a heading level when text tagging is already on, and SetAutoTagMode clears any queued level when it runs. Register the heading first and switch the mode on afterwards, and the title comes out as a plain paragraph with no error anywhere to tell you
Why does a heading wait through the image drawn before it?
RegisterHeading names the level for the next piece of text, and only text can use it up. If an image is drawn in between, the image becomes a figure and the heading stays queued for the text that follows. The obvious implementation lets any tag consume the queued level, and then the common sequence of registering a section heading, drawing a banner image, and drawing the title produces a banner tagged as H2 and a real title demoted to a paragraph. A reader navigating by headings lands on a picture
Two more things follow from how the queue works. The heading level attaches to whatever text comes next, whatever string you passed to RegisterHeading: if your layout draws a small "Section 3" kicker above the title, the kicker becomes the heading. And levels deeper than six are written as H6, since a reader knows nothing past that. The Title argument still does its own job, feeding the outline that DrawTOC later turns into a table of contents
Images get the same "spent on one item" treatment. RegisterFigure supplies the description the next image carries, and RegisterDecoration declares the next image a rule, border or background with no meaning, which is then written as a Layout artifact (ISO 32000-1 §14.8.2.2). Each declaration is consumed by exactly one image, so a later image never inherits a description written for an earlier one. Alt text attached to the wrong picture is worse than none, because the reader states something false with full confidence
We deliberately did not try to infer which images are decorative. An image is either content the reader should describe or content it should skip, and only the calling code knows which. The case to avoid is the third one: an image drawn with neither declaration becomes a figure with no description, which fails PDF/UA and makes the screen reader stop to say "graphic" and then nothing. If you grep your report code for DrawImage, every hit should have a RegisterFigure or RegisterDecoration right above it
Lib.RegisterHeading(2, 'Gearbox inspection'); // queued for the next text
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // loads and selects the image
Lib.DrawImage(72, 160, 360, 270); // becomes a tagged Figure
Lib.RegisterDecoration; // meaningless rule
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4); // drawn inside a layout artifact
Lib.DrawText(72, 480, 'Gearbox inspection'); // only now does the H2 appear
The description deserves more care than any other string in the document. A non-sighted reader gets it in place of the picture, and that is all they get. "Chart" tells them nothing; "Quarterly revenue by region, with the eastern region highest in Q3" tells them what a sighted reader takes from the image in two seconds
Tables: header rows are declared, never guessed
With the table bit on, DrawTableRows writes the table, its rows and its cells into the structure tree, so a reader can tell which column a value sits in instead of reading the grid out as unrelated text. SetTableHeaderRowCount says how many leading rows are headers. Those rows are written as TH cells with a column scope, which is what lets a reader announce "Torque" before the value the user has moved to
The header row count defaults to zero, and that was a choice. The first row of a table usually is the header, so guessing would be right most of the time. The cost of the wrong guess is lopsided, though: a data row mistaken for a header gets read aloud in front of every value in its column, which is far more confusing than a table with no headers at all. One line of code from you is cheaper than that failure mode
Declaring header rows and repeating them on each page are separate decisions. SetTableHeaderRowCount is semantic and changes nothing on the page. Repetition is layout, and it belongs to the RepeatHeaderRows argument of DrawTaggedTableRows. If the two were one setting, asking for correct header tags would also start drawing repeated header bands you never asked for, and a layout that had been checked page by page would shift. Rows passed as RepeatHeaderRows are tagged as headers too, so you do not have to say the same thing twice
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // row 1 is the header band
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... fill the data rows ...
// Draw rows 1..40 into a 600pt band, repeating one header row per page
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Hand-tagged sections inside an automatic document
Automatic tagging stands aside while a tag you opened by hand is still open. That lets your code describe part of a document itself and leave the rest to the library without the two nesting into each other, which is the arrangement most real documents want. The cover page and the signature block have structure only you understand. The two hundred pages of body text between them do not
Three rules keep the output valid whatever mix you use. Nothing is tagged inside an artifact, because the marked-content sequence for an artifact cannot also carry a structure element, and a file that tries fails PDF/UA validation. Empty text opens no element, so a stray DrawText with an empty string cannot leave behind an element that a reader announces as a blank. And a library call that draws through other drawing calls internally opens one tag at the outer level, so its pieces never end up nested inside a second element of their own
After saving, read the tree back before you trust it. The read API is 1-based, which caught us out: our first check looped from 0 to StructTreeItemCount - 1, got an empty record at the start, never reached the last element, and for a while we were convinced a paragraph had gone missing. Nothing had. The loop was just one off at both ends
var
I: Integer;
begin
// A pull quote tagged by hand; automatic tagging stays out of it
Lib.BeginTag('BlockQuote', '', '');
Lib.DrawWrappedText(72, 520, 450,
'All readings were taken with the unit at operating temperature.');
Lib.EndTag;
Lib.SaveToFile('report.pdf');
// Read the tree back: indexes run from 1 to StructTreeItemCount
if Lib.LoadFromFile('report.pdf', '') = 1 then
for I := 1 to Lib.StructTreeItemCount do
Writeln(Lib.GetStructTreeItemText(I, 2), ' ', // resolved type
Lib.GetStructTreeItemText(I, 4)); // /Alt, if any
end;
A listing like this is a quick sanity check, not a verdict. What you want to see is an H1 before the first paragraph, each figure carrying its description, and no figure with an empty /Alt
When drawing order and reading order part ways
Automatic tagging records elements in the order content is drawn. If your layout code draws the sidebar before the body, that is the order the tree holds, and that is the order a screen reader reads. For documents where visual order and reading order genuinely differ, or that need other structure types or per-element language, the manual tagging API is the better fit, and the walkthrough of tagged PDF and accessibility structure covers roles, scopes and header bindings in detail
For pages you did not draw yourself, such as imported pages with no marked content, AutoTagPages works after the fact: it takes the median text size across the pages it is given as body text and treats markedly larger runs as headings, and marks other painting as artifacts. It is the right tool for content you cannot regenerate. For anything your own code draws, we prefer tagging at draw time, because there the library is told what a heading is instead of guessing from font size
Once the document is finished, run it through a checker instead of assuming: the notes on PDF/A and PDF/UA preflight show how to get a verdict on the structure you produced, and the article on dataset-driven report export shows where these calls sit in a report engine that builds its layout from data
Because PDF Library for Delphi is written in native Pascal for Delphi, C++Builder and Lazarus, with no external PDF runtime to ship, the tagged output comes from the same code that draws the page. The complete API reference and platform list are on the PDF Library for Delphi product page