Technisch artikel

Automatische structuurtags voor toegankelijke PDF in Delphi

Open een gegenereerd rapport in een screen reader en de gebruikelijke falingen tonen zich binnen een minuut: "Pagina 3 van 40" wordt op elke pagina opgelezen, het bedrijfslogo wordt aangekondigd als "graphic" met niets erachter, en een tabel komt eruit als één lange reeks getallen zonder kolomnamen. PDF Library for Delphi voorkomt alle drie met automatische structuurtagging: roep SetAutoTagMode één keer aan, en gewone tekenaanroepen produceren paragrafen, koppen, figures, tabellen en overslaanbare paginameubelatuur terwijl het document wordt getekend

De handmatige route bestaat nog steeds. U kunt elke tekenaanroep in BeginTag en EndTag wikkelen, en voor documenten met ongebruikelijke structuur is dat het juiste gereedschap. Voor een gewoon rapport, factuur of afschrift maakt handmatige tagging toegankelijkheid echter afhankelijk van niemand die ooit een paar vergeet, in elke codepath die iets tekent, zolang het product bestaat. Daar zetten we geen compliance-eis op

Wat SetAutoTagMode aanzet

SetAutoTagMode neemt een bit-masker en geeft de modus terug die vóór de aanroep gold. Elke bit dekt één soort content:

  • AUTOTAG_TEXT (1): tekst wordt een paragraaf, of een kop wanneer er één is geregistreerd
  • AUTOTAG_FURNITURE (2): lopende kopregels, voetregels en paginanummers worden artifacts die een reader overslaat
  • AUTOTAG_FIGURE (4): een getekende afbeelding wordt een figure, of een artifact wanneer u hem decoratief verklaarde
  • AUTOTAG_TABLE (8): getekende tabellen dragen hun rijen en cellen mee in de structuurboom
  • AUTOTAG_DEFAULT (15): alle vier, en de waarde die we voor nieuwe rapportcode aanbevelen

De modus aanzetten markeert het document ook als getagd, en die stap telt zwaarder dan hij lijkt. Onder ISO 32000-1 §14.7.1 behandelt een reader een bestand als ongetagd tenzij de /MarkInfo-dictionary van de catalog /Marked true zegt. Een bestand kan een complete, correcte structuurboom dragen en nog als structuurloos worden aangekondigd, omdat de ene vlag die assistieve technologie laat zoeken ontbreekt. De boom is er. Niets leest hem

Teksttagging werkt per aanroep. Elke DrawText-, DrawWrappedText- of DrawTextBox-aanroep wordt één element, dus een paragraaf die regel voor regel met vijf DrawText-aanroepen is getekend, bereikt de reader als vijf paragrafen van één regel. Breekt uw indelingscode de regels al zelf, dan loont het om dat te veranderen: geef de hele paragraaf aan DrawWrappedText en laat één aanroep één paragraaf produceren

Diagram van SetAutoTagMode-bitmaskers die gewone Delphi-tekenaanroepen afbeelden in een getagde PDF-structuurboom, met de Marked-vlag gemarkeerd als vereist
Elke actieve bit van SetAutoTagMode leidt gewone tekenaanroepen om naar een overeenkomstig element van de structuurboom
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // oorsprong linksboven, y loopt naar beneden
    Lib.SetDocumentLanguage('en-US');
    Lib.SetAutoTagMode(AUTOTAG_DEFAULT);       // vóór elke RegisterHeading-aanroep
    Lib.AddStandardFont(4);

    Lib.SetTextSize(18);
    Lib.RegisterHeading(1, 'Annual service report');
    Lib.DrawText(72, 96, 'Annual service report');        // getagd als 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.');  // één P

    // ... resterende pagina’s ...

    // Loopt over elke bestaande pagina, dus roep hem aan zodra de pagina’s getekend zijn
    Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1);  // Pagination-artifact
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Het commentaar op de derde regel staat er niet voor niets. RegisterHeading zet een kopniveau alleen in de wachtrij wanneer teksttagging al aan staat, en SetAutoTagMode wist elke stilstaande niveau-opgave wanneer hij draait. Registreert u de kop eerst en zet u de modus daarna aan, dan komt de titel eruit als een kale paragraaf zonder dat ergens een foutmelding u dat vertelt

Waarom wacht een kop tot de afbeelding ervoor voorbij is?

RegisterHeading benoemt het niveau voor de volgende tekst, en alleen tekst kan hem verbruiken. Wordt tussenin een afbeelding getekend, dan wordt die een figure en blijft de kop in de wachtrij voor de tekst die volgt. De voor de hand liggende implementatie laat elke tag het wachtende niveau verbruiken, en dan produceert de gewone reeks van een sectiekop registreren, een bannerafbeelding tekenen en de titel tekenen een banner die als H2 is getagd en een echte titel die tot paragraaf is gedegradeerd. Een reader die langs koppen navigeert belandt op een plaatje

Nog twee dingen volgen uit hoe de wachtrij werkt. Het kopniveau hecht aan welke tekst ook het eerst komt, welke string u ook aan RegisterHeading doorgaf: tekent uw indeling een klein "Sectie 3"-voetspoor boven de titel, dan wordt het voetspoor de kop. En niveaus dieper dan zes worden als H6 weggeschreven, want een reader weet niets daarvoorbij. Het Title-argument doet zijn eigen werk nog steeds en voedt de outline die DrawTOC later tot een inhoudsopgave verwerkt

Afbeeldingen krijgen dezelfde "opgemaakt aan één item"-behandeling. RegisterFigure levert de beschrijving die de volgende afbeelding draagt, en RegisterDecoration verklaart de volgende afbeelding een lijn, rand of achtergrond zonder betekenis, die dan als Layout-artifact wordt weggeschreven (ISO 32000-1 §14.8.2.2). Elke verklaring wordt door precies één afbeelding verbruikt, dus een latere afbeelding erft nooit een beschrijving die voor een eerdere is geschreven. Alt-tekst aan het verkeerde plaatje hangen is erger dan geen, want de reader zegt dan iets onwaars met volle overtuiging

We hebben bewust niet geprobeerd af te leiden welke afbeeldingen decoratief zijn. Een afbeelding is ofwel content die de reader moet beschrijven, ofwel content die hij moet overslaan, en alleen de aanroepende code weet welke. Het te vermijden geval is het derde: een afbeelding getekend zonder enige verklaring wordt een figure zonder beschrijving, wat PDF/UA faalt en de screen reader doet stoppen om "graphic" te zeggen en dan niets meer. Grijpt u uw rapportcode door op DrawImage, dan hoort boven elke treffer een RegisterFigure of RegisterDecoration te staan

Diagram van PDF Library for Delphi met een geregistreerde PDF-kop die wacht tijdens een tussenliggende afbeelding, terwijl RegisterFigure en RegisterDecoration elk aan precies één afbeelding worden besteed
RegisterHeading wacht op de volgende tekst, ongeacht wat er intussen nog meer wordt getekend
Lib.RegisterHeading(2, 'Gearbox inspection');   // in de wachtrij voor de volgende tekst

Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0);       // laadt en selecteert de afbeelding
Lib.DrawImage(72, 160, 360, 270);             // wordt een getagde Figure

Lib.RegisterDecoration;                       // betekenisloze lijn
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4);               // getekend binnen een layout-artifact

Lib.DrawText(72, 480, 'Gearbox inspection');  // pas nu verschijnt de H2

De beschrijving verdient meer zorg dan elke andere string in het document. Een niet-ziende reader krijgt haar in plaats van het plaatje, en dat is alles wat hij krijgt. "Grafiek" vertelt hem niets; "Kwartaalomzet per regio, met het oosten het hoogst in Q3" vertelt hem wat een ziende reader in twee seconden uit de afbeelding haalt

Tabellen: koprijen worden verklaard, nooit geraden

Met de tabelbit aan schrijft DrawTableRows de tabel, zijn rijen en zijn cellen in de structuurboom, zodat een reader kan zien in welke kolom een waarde zit in plaats van het raster als losse tekst op te lezen. SetTableHeaderRowCount zegt hoeveel voorlooprijen koppen zijn. Die rijen worden als TH-cellen met kolombereik weggeschreven, en dat laat een reader "Torsie" aankondigen vóór de waarde waar de gebruiker naartoe is gegaan

Het aantal koprijen staat standaard op nul, en dat was een keuze. De eerste rij van een tabel is meestal de kop, dus raden zou meestal goed uitpakken. De prijs van de verkeerde gok ligt eenzijdig: een datarij die voor een kop wordt aangezien, wordt vóór elke waarde in zijn kolom opgelezen, wat veel verwarrender is dan een tabel zonder koppen helemaal. Eén regel code van u is goedkoper dan dat faalscenario

Koprijen verklaren en ze op elke pagina herhalen zijn aparte beslissingen. SetTableHeaderRowCount is semantisch en verandert niets op de pagina. Herhaling is indeling en hoort bij het RepeatHeaderRows-argument van DrawTaggedTableRows. Zouden de twee één instelling zijn, dan zou om correcte kop-tags vragen ook herhaalde kopbanden gaan tekenen die u nooit vroeg, en zou een indeling die pagina voor pagina was gecontroleerd verschuiven. Rijen die als RepeatHeaderRows worden doorgegeven worden ook als koppen getagd, dus u hoeft hetzelfde niet twee keer te zeggen

Diagram van PDF Library for Delphi met tabelkopcellen die kolombereik dragen in de structuurboom, terwijl RepeatHeaderRows herhaling bij een pagina-einde een puur lay-outbesluit houdt
Koprijen staan één keer in de structuurboom, met kolombereik gekoppeld aan elke TH-cel
var
  TableID: Integer;
begin
  TableID := Lib.CreateTable(40, 3);
  Lib.SetTableHeaderRowCount(TableID, 1);       // rij 1 is de kopband
  Lib.SetTableCellContent(TableID, 1, 1, 'Part');
  Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
  Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
  // ... vul de datarijen ...
  // Teken rij 1..40 in een band van 600pt, met per pagina één herhaalde kopregel
  Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;

Handmatig getagde secties binnen een automatisch document

Automatische tagging houdt zich afzijdig zolang een tag die u met de hand opende nog open staat. Dat laat uw code een deel van een document zelf beschrijven en de rest aan de library overlaten zonder dat de twee in elkaar nesten, wat is wat de meeste echte documenten willen. De omslagpagina en het handtekeningenblok hebben structuur die alleen u begrijpt. De tweehonderd pagina’s lopende tekst ertussen niet

Drie regels houden de uitvoer geldig welke mix u ook gebruikt. Binnen een artifact wordt niets getagd, want de marked-content-reeks van een artifact kan geen structurelement meedragen, en een bestand dat het probeert faalt PDF/UA-validatie. Lege tekst opent geen element, dus een verdwaalde DrawText met een lege string kan geen element nalaten dat een reader als blanco aankondigt. En een library-aanroep die intern door andere tekenaanroepen heen tekent, opent één tag op het buitenste niveau, zodat zijn onderdelen nooit zelf in een tweede element terechtkomen

Sla op, en lees de boom terug voordat u hem vertrouwt. De lees-API is 1-based, en dat ving ons: onze eerste controle lusde van 0 tot StructTreeItemCount - 1, kreeg een leeg record aan het begin, bereikte het laatste element nooit, en een tijdje waren we ervan overtuigd dat er een paragraaf was verdwenen. Niets was verdwenen. De lus zat alleen aan beide einden één ernaast

var
  I: Integer;
begin
  // Een pull quote met de hand getagd; automatische tagging blijft erbuiten
  Lib.BeginTag('BlockQuote', '', '');
  Lib.DrawWrappedText(72, 520, 450,
    'All readings were taken with the unit at operating temperature.');
  Lib.EndTag;
  Lib.SaveToFile('report.pdf');

  // Lees de boom terug: indexen lopen van 1 tot StructTreeItemCount
  if Lib.LoadFromFile('report.pdf', '') = 1 then
    for I := 1 to Lib.StructTreeItemCount do
      Writeln(Lib.GetStructTreeItemText(I, 2), '  ',    // opgelost type
              Lib.GetStructTreeItemText(I, 4));         // /Alt, indien aanwezig
end;

Een opsomming als deze is een snelle gezondheidscontrole, geen oordeel. Wat u wilt zien is een H1 vóór de eerste paragraaf, elk figure met zijn beschrijving, en geen figure met een lege /Alt

Wanneer tekenvolgorde en leesvolgorde uit elkaar gaan

Automatische tagging legt elementen vast in de volgorde waarin content wordt getekend. Tekent uw indelingscode de zijbalk vóór de hoofdtekst, dan is dat de volgorde die de boom vasthoudt, en de volgorde waarin een screen reader voorleest. Voor documenten waar visuele en leesvolgorde werkelijk verschillen, of die andere structuurtypen of per-element taal nodig hebben, is de handmatige tagging-API de betere match, en de doorloop van getagde PDF en toegankelijkheidsstructuur behandelt rollen, bereiken en koppelingen van koppen in detail

Voor pagina’s die u niet zelf tekende, zoals geïmporteerde pagina’s zonder marked content, werkt AutoTagPages achteraf: hij neemt de mediane tekstgrootte over de gegeven pagina’s als bodytekst, behandelt duidelijk grotere reeksen als koppen en markeert ander schilderwerk als artifacts. Het is het juiste gereedschap voor content die u niet kunt regenereren. Voor alles wat uw eigen code tekent verkiezen we tagging op tekentijd, omdat de library daar wordt verteld wat een kop is in plaats van uit fontgrootte te raden

Is het document af, dan draait u het door een checker in plaats van het aan te nemen: de aantekeningen bij PDF/A- en PDF/UA-preflight laten zien hoe u een oordeel krijgt over de structuur die u produceerde, en het artikel over datasetgedreven rapportexport laat zien waar deze aanroepen zitten in een rapportengine die zijn indeling uit data bouwt

Omdat PDF Library for Delphi in native Pascal is geschreven voor Delphi, C++Builder en Lazarus, zonder externe PDF-runtime om mee te sturen, komt de getagde uitvoer uit dezelfde code die de pagina tekent. De volledige API-referentie en platformlijst staan op de productpagina van PDF Library for Delphi