Teknisk artikel

Opbygning af taggede PDF-strukturelementer i Delphi med PDF Library for Delphi

En tilgængelig PDF hviler på én struktur, som den synlige side aldrig viser: strukturtræet defineret i ISO 32000-1 §14.7. Det er et logisk hierarki af overskrifter, afsnit, tabeller og figurer, lagt oven på det tegnede indhold og afbildet på standardroller gennem et role map. En skærmlæser læser det træ, ikke mærkerne på siden. Uden det er en genereret faktura, der ser upåklagelig ud, semantisk tom, for indholdsstrømmen registrerer tegnerækkefølge og intet andet. Totalen kan blive læst op før varelinjerne, sidefoden kan skære ind i et afsnit, varetabellen kan falde sammen til én udifferentieret strøm af ord. Prisen for at forhindre det er skæv til din fordel. At udsende struktur, mens du tegner, er nogle minutters kode; at eftermontere den i færdige dokumenter er et afhjælpningsprojekt. losLab PDF Library (PDF Library for Delphi) blotlægger træet for Delphi og C++Builder gennem et lille sæt kald, der pakker hver tegneoperation ind i dens logiske rolle

Hvordan marked content bindes til strukturtræet

To lag samarbejder. I indholdsstrømmen sættes tegneoperationer i parentes som marked content-sekvenser, der hver bærer et heltals-MCID. I dokumentkataloget afbilder strukturtræet de MCID'er ind i et hierarki af typede elementer (H1, P, Table, Figure) med attributter som alternativ tekst og sprog. Brugerdefinerede elementtyper er tilladt, men hver enkelt skal kunne opløses til en standardrolle gennem role map'et (ISO 32000-1 §14.8.4). Indhold, der slet ingen mening bærer, som streger, baggrunde og gentaget sidepynt, markeres som artefakt, så hjælpeteknologi springer det over i stedet for at læse det op midt i en sætning

PDF Library for Delphi vedligeholder begge lag bag ét parentespar. BeginTag åbner et strukturelement og starter marked content-sekvensen, tegnekaldene lander inde i den, og EndTag lukker begge. Det bogholderi, der spænder ben for håndlavet tagging, altså MCID'erne og forældretræet og sidereferencerne, sker internt, hvor du ikke kan gøre det forkert

PDF Library for Delphi-diagram, der binder marked-content-forløb med heltals-MCID'er til et H1-, P- og Figure-strukturtræ gennem role map, med artefakter udelukket fra læserækkefølgen
Heltals-MCID'er binder marked-content-forløb til et typet strukturtræ, mens rollekortet opløser brugerdefinerede roller, og artefakter forbliver uden for læserækkefølgen

To kontakter på dokumentniveau rammer arbejdet ind, før noget tag overhovedet åbnes. SetMarkInfo skriver det katalogflag, der erklærer dokumentet tagget, og IsTaggedPDF læser det tilbage, hvilket er den billige første sondering, når man skal afgøre, om en indkommen fil har nogen struktur værd at bevare. Sprog har to indgange. SetDocumentLanguage sætter dokumentets standard for sig selv, mens SetPDFUAMode sætter det som led i at slå fuldt PDF/UA-output til. En fil kan være nyttigt tagget uden at påberåbe sig PDF/UA-konformitet, og en faseopdelt udrulning starter ofte netop dér

Tagging mens du tegner, ikke bagefter

Det genereringsmønster, der virker, er at behandle tag-parentesen som en del af hvert tegnekalds signatur, aldrig som en senere gennemgang:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // origo øverst til venstre
    Lib.SetPDFUAMode('en-US');                 // hæver gemmeversionen til PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title er obligatorisk for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // brugerdefineret type -> standardrolle
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // sidepynt: udelukket fra oplæsning
    // ... tegn streger og baggrundstoning ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Tre kald i den sekvens vejer tungt for compliance. SetPDFUAMode slår PDF/UA-output til og hæver stiltiende dokumentversionen til PDF 1.7, hvilket støder sammen med fastlåsning af versionen. Et dokument låst til PDF 1.4 med LockSaveVersion nægter at blive gemt og returnerer fejlkode 602, når først UA-tilstanden er aktiv, et sammenstød der plejer at dukke op, når arkivprofiler og tilgængelighedskrav bliver konfigureret af forskellige teams. SetInformation(1, ...) skriver dokumenttitlen, som ISO 14289 forventer at fremvisere viser i stedet for filnavnet; dens fravær er et af de hyppigste PDF/UA-fund derude. AddRoleMap registrerer den brugerdefinerede type ManualTitle som en H1, og springer man det over, vil diagnostikken beskrevet nedenfor flage en ikke-afbildet rolle

Overskriftsniveauer fortjener en bevidst politik, ikke ad hoc-valg truffet efter, hvordan en side ser ud. Skærmlæserbrugere hopper mellem afsnit via overskriftsgenveje, så en skabelon, der går fra H1 til H3, fordi mellemniveauet så for stort ud i det visuelle design, ødelægger stille og roligt den navigation, og ingen visuel gennemgang vil nogensinde fange det. Det er præcis den defekt, diagnosen HEADING-LEVEL-SKIP findes for at sætte navn på. Afbild hver skabelons visuelle stilarter på en fast overskriftsstige én gang, ét sted, og driften starter aldrig

Tabeller, en skærmlæser rent faktisk kan navigere i

Tegnede gitterlinjer betyder ingenting uden for skærmen. Det, skærmlæsere navigerer i, er strukturelle relationer: hvilke celler der er hoveder, hvad hvert hoved styrer, og hvordan dataceller bindes til hoveder i uregelmæssige layouts. Attributkaldene på strukturelementer håndterer alle tre:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // kun gyldigt mens denne TH er åben
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // hovedet spænder over værdi- og enhedskolonnen
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // eksplicit binding til uregelmæssige tabeller
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Rækkefølgereglen er striks og håndhæves i tavshed. Hvert SetStructElem*-kald gælder det tag, der er åbent i netop det øjeblik, mellem dets BeginTag og dets EndTag, og det returnerer 0 uden at rejse noget, når intet tag er åbent, eller attributten ikke gælder for det aktuelle. Et fejlplaceret kald forsvinder ganske enkelt. At pakke returværdierne ind i assertions under udviklingen fanger driften, mens du stadig kan se den; lader man den ligge, dukker et manglende scope først op, når en tilgængelighedsrevision kører en rigtig skærmlæser hen over tabellen. De element-ID'er, der sendes gennem BeginTagEx2, fodrer ID-træet (ISO 32000-1 §14.7.4), og det er det, der overhovedet gør bindingen i SetStructElemHeaders mulig at opløse

Den samme attributfamilie dækker resten af det, hjælpeteknologi læner sig op ad. SetStructElemListNumbering erklærer, hvordan listeelementer er mærket, så en skærmlæser oplyser positionen i listen i stedet for at remse punkttegn op. SetStructElemBBox registrerer afgrænsningsrammen for figurer og tabeller, som reflow-visninger bruger til at placere indhold. SetStructElemActualText leverer erstatningstekst til strøg, hvis glyffer ikke afbildes til læsbare tegn, som en initial sat sammen af vektorgrafik. Hver af dem følger samme regel: den binder sig til det åbne tag, eller den forsvinder

PDF Library for Delphi-tabeldiagram, der viser TH-scope, en colspan på to og en headers-attribut bundet til dataceller, ved siden af reglen om, at attributkald kun binder, mens deres tag er åbent
Skærmlæsere følger TH scope-, colspan- og headers-bindinger i stedet for tegnede linjer, og attributkald binder kun, mens deres tag er åbent

Artefakter, sprog og diagnostikporten før gemning

Gentaget sidepynt, altså løbende sidehoveder, falsemærker, vandmærker og baggrundstoninger, hører hjemme inde i BeginArtifact- og EndArtifact-parenteser, så det aldrig kommer ind i læsestrømmen. Sprog kan nedarves. Dokumentets standard kommer fra argumentet til SetPDFUAMode, og et strøg på et andet sprog tilsidesætter den per element gennem BeginTagEx eller SetStructElemLang. Det er det, der holder et fransk citat inde i en engelsk manual udtaleligt

Før gemning kører GetPDFUADiagnostics bibliotekets strukturelle tjek hen over dokumentet i hukommelsen og returnerer fund som tekst, hvor en tom streng betyder, at intet blev fundet. Koderne sætter direkte navn på de klassiske forfatterfejl: FIGURE-NO-ALT for et billede uden alternativ tekst, HEADING-LEVEL-SKIP for en H3, der følger en H1, ROLEMAP-UNMAPPED for en brugerdefineret type, der aldrig blev registreret. Kobl dette ind i builden (generér dokumentsættet, fæld trinnet ved ikke-tom diagnostik), og tilgængelighedsregressioner bliver til fejl af compile time-typen i stedet for revisionsfund måneder senere. Den fulde konformitetsdom hører stadig til hos preflight på den gemte fil, dækket i PDF/A- og PDF/UA-preflight i Delphi, for nogle normaliseringer anvendes først under serialiseringen

Navigation i annotationer har sin egen knap. PDF/UA forventer, at gennemløb af formularfelter og links med tastatur følger strukturrækkefølgen, og SetTabOrderMode skriver den tabulatorrækkefølge på sideniveau, som fremvisere respekterer, mens GetTabOrderMode står klar til revision af indkomne filer. Det er den slags krav, ingen bemærker, før en bruger, der kun benytter tastatur, melder fejlen, og det koster ét kald per dokument at få rigtigt

Strukturtræer overlever ikke enhver fletning

Taggede dokumenter forbliver kun taggede, når hvert senere behandlingstrin bevarer træet, og den skarpe kant inde i PDF Library for Delphi er flettelistefamilien. MergeFileListFast bytter bevarelsen af strukturtræet for hastighed. Det er den rigtige handel til batches af scannede billeder og den forkerte til taggede rapporter, for outputtet åbner fint, gengives identisk og har stille og roligt mistet sit tilgængelighedslag. Brug standarden MergeFileList eller den strikse variant, hver gang bare ét input er tagget, og gør IsTaggedPDF til en del af assertions efter samlingen, så et fladtrykt batch ikke kan sendes af sted, uden at nogen bemærker det. Samlepipelines til store dokumentsæt bærer flere afvejninger af den slags, som udforskes i fletning, opdeling og direct access for store PDF'er

PDF Library for Delphi-diagram over GetPDFUADiagnostics, der returnerer en tom streng eller navngivne findings som FIGURE-NO-ALT, der fejler builden, før preflight dømmer den gemte fil
GetPDFUADiagnostics rapporterer fund som FIGURE-NO-ALT før gemning, og et ikke-tomt resultat koblet til buildet får trinnet til at fejle straks

Verifikationsløkken lukkes uden for biblioteket: åbn outputtet i Acrobat, inspicér tag-panelet, og læs mindst ét dokument per skabelonfamilie med en rigtig skærmlæser. Diagnostikken fanger strukturelle fejl; kun et menneskeligt øre fanger en læserækkefølge, der er teknisk gyldig og i praksis forvirrende. Evalueringsbuilds og den komplette API-reference for tagging findes på produktsiden for losLab PDF Library til Delphi