Teknisk artikel

Att bygga taggade PDF-strukturträd i Delphi med PDFlibPas

En tillgänglighetsanpassad (accessible) PDF vilar på (rests on) en struktur som den synliga sidan aldrig visar: det strukturträd (structure tree) som definieras i ISO 32000-1 §14.7. Det är en logisk hierarki av rubriker, stycken (paragraphs), tabeller och figurer, lagrad över det ritade (painted) innehållet och mappad till standardroller genom en roll-karta (role map). En skärmläsare (screen reader) läser det trädet, inte märkena (marks) på sidan. Utan den är en genererad faktura som ser obefläckad (immaculate) ut semantiskt tom, eftersom innehållsströmmen (content stream) bara registrerar (records) ritordning och ingenting annat. Totalsumman kan läsas upp (announced) före artikelraderna (line items), sidfoten (footer) kan skära in (cut into) i ett stycke, artikel-tabellen kan kollapsa till en enda odifferentierad (undifferentiated) ramsa av ord (run of words). Kostnaden för att förhindra det är skev (lopsided) till din fördel. Att emittera (Emitting) struktur medan du ritar handlar om minuter av kod; att eftermontera (retrofitting) den i färdiga dokument är ett saneringsprojekt (remediation project). losLab PDF Library (PDFlibPas) exponerar trädet för Delphi och C++Builder genom en liten uppsättning (set) anrop som omsluter (wrap) varje ritnings-operation i dess logiska roll

Hur markerat innehåll (marked content) binder till strukturträdet

Två lager (layers) samarbetar. I innehållsströmmen (content stream) är ritnings-operationer (drawing operations) inneslutna i (bracketed into) markerade innehålls-sekvenser (marked-content sequences), där var och en (each) bär på ett heltal, ett MCID. I dokumentkatalogen (document catalog) mappar strukturträdet de där MCID:na till en hierarki av typade element (H1, P, Table, Figure) med attribut som alternativtext (alternate text) och språk. Anpassade elementtyper (Custom element types) är lagliga, men varje sådan måste upplösas (resolve) till en standardroll genom roll-kartan (ISO 32000-1 §14.8.4). Innehåll som inte bär någon betydelse alls, som linjer (rules), bakgrunder och upprepad sidoinredning (repeated page furniture), markeras som en artefakt (artifact) så att hjälpmedel (assistive technology) hoppar över (skips) det i stället för att läsa det mitt i en mening

PDFlibPas underhåller (maintains) båda lagren bakom ett och samma inramningspar (bracket pair). BeginTag öppnar ett strukturelement och startar den markerade innehålls-sekvensen, ritningsanrop (drawing calls) landar inuti det, och EndTag stänger båda. Bokföringen (bookkeeping) som fäller (trips up) handskriven taggning (hand-rolled tagging) – MCID:na, föräldraträdet (parent tree) och sidreferenserna – sker internt där du inte kan göra fel (get it wrong)

Två växlar på dokumentnivå (document-level switches) ramar in (frame) arbetet innan någon tagg öppnas. SetMarkInfo skriver den katalogflagga (catalog flag) som förklarar (declares) dokumentet som taggat, och IsTaggedPDF läser tillbaka den, vilket är den billiga första sonden (probe) när man beslutar huruvida (whether) en inkommande (inbound) fil har någon struktur värd att bevara. Språk har två ingångspunkter. SetDocumentLanguage sätter dokumentstandarden (document default) på egen hand, medan SetPDFUAMode sätter den som en del av att aktivera (enabling) fullständig PDF/UA-utmatning (output). En fil kan med fördel (usefully) taggas utan att göra anspråk på PDF/UA-överensstämmelse (conformance), och en fasad utrullning (phased rollout) börjar ofta exakt där

Taggning under (while) ritning, inte efteråt

Det genererings-mönster (generation pattern) som fungerar är att behandla tagg-inramningen (tag bracket) som en del av varje ritningsanrops signatur, aldrig som ett senare pass (later pass):

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // top-left origin
    Lib.SetPDFUAMode('en-US');                 // bumps the save version to PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title is mandatory for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // custom type -> standard role
    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');               // page decoration: excluded from reading
    // ... draw rules and background tint ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Tre anrop i den sekvensen bär överensstämmelse-tyngd (compliance weight). SetPDFUAMode aktiverar PDF/UA-utmatning och puttar i tysthet upp dokumentversionen till PDF 1.7, vilket krockar med versions-låsning (version pinning). Ett dokument låst till PDF 1.4 med LockSaveVersion vägrar att spara och returnerar felkod 602 så snart (once) UA-läget är aktivt, en krock som tenderar att flyta upp (surface) när arkiv-profiler och tillgänglighetskrav (accessibility requirements) konfigureras av olika team. SetInformation(1, ...) skriver dokumentets titel, vilken ISO 14289 förväntar sig att visare (viewers) ska visa i stället för filnamnet; dess frånvaro är ett av de vanligaste PDF/UA-fynden i det vilda (in the wild). AddRoleMap registrerar den anpassade ManualTitle-typen som en H1:a, och att hoppa över det lämnar diagnostiken (diagnostics) som beskrivs nedan med att flagga en omappad roll (unmapped role)

Rubriknivåer (Heading levels) förtjänar en medveten (deliberate) policy, inte ad-hoc-val (ad-hoc choices) gjorda för utseendet (look) på en sida. Skärmläsaranvändare hoppar mellan sektioner med genvägar för rubriker (heading shortcut), så en mall som går från H1 till H3 eftersom mellannivån såg för stor ut i den visuella designen, bryter i tysthet (quietly breaks) den navigeringen, och ingen visuell granskning (review) kommer någonsin att fånga det (catch it). Det är exakt den defekt som diagnostiken HEADING-LEVEL-SKIP existerar för att namnge (name). Mappa varje malls visuella stilar (visual styles) till en fast (fixed) rubrik-stege (heading ladder) en gång (once), på ett ställe, och driften (drift) börjar aldrig ens

Tabeller som en skärmläsare faktiskt kan navigera

Ritade rutnätslinjer (grid lines) betyder ingenting (nothing) utanför (off) skärmen. Det skärmläsare navigerar är strukturella relationer: vilka celler som är rubriker (headers), vad varje rubrik styr (governs), och hur dataceller binder till rubriker i oregelbundna (irregular) layouter. Attribut-anropen för strukturelement (structure-element attribute calls) hanterar alla tre:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicit binding for irregular tables
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Ordningsregeln (ordering rule) är strikt och upprätthålls (enforced) i tystnad. Varje SetStructElem*-anrop gäller (applies to) för den tagg som är öppen just då, mellan dess BeginTag och dess EndTag, och returnerar 0 utan att utlösa (raising) någonting när ingen tagg är öppen eller om (or) attributet inte gäller för den nuvarande. Ett felplacerat (misplaced) anrop försvinner helt enkelt (simply vanishes). Att omsluta (Wrapping) returvärdena i (in) assertioner (assertions) under utveckling fångar (catches) driften (drift) medan du fortfarande kan se den; lämnad i fred (left alone), visar sig en saknad räckvidd (missing scope) först när en tillgänglighetsgranskning (accessibility audit) kör en riktig skärmläsare tvärs över (across) tabellen. De element-ID:n som skickas genom (passed through) BeginTagEx2 matar ID-trädet (feed the ID tree, ISO 32000-1 §14.7.4), och det är vad som gör SetStructElemHeaders-bindningen upplösningsbar (resolvable) över huvud taget (in the first place)

Samma attribut-familj täcker (covers) resten av det som hjälpmedel (assistive technology) lutar sig (leans) mot. SetStructElemListNumbering deklarerar hur listobjekt är märkta (labeled), så att en skärmläsare läser upp positionen (position) inom listan i stället för att rabbla upp (reciting) punkt-glyfer (bullet glyphs). SetStructElemBBox registrerar (records) begränsningsrutan (bounding box) för figurer och tabeller, vilket omflödes-vyer (reflow views) använder för att placera innehåll. SetStructElemActualText levererar (supplies) ersättningstext (replacement text) för flöden (runs) vars glyfer inte mappar till läsbara tecken (readable characters), såsom en anfang (drop cap) sammansatt från vektorgrafik (vector art). Varje sådan (Each one) följer samma regel: den binder till den öppna taggen, eller så försvinner (vanishes) den

Artefakter (Artifacts), språk, och grindvakten (gate) för diagnostik före sparande (pre-save diagnostics)

Upprepad sidoinredning (page furniture), vilket betyder sidhuvuden/sidfötter (running headers), vikmärken (fold marks), vattenstämplar och bakgrundstoningar (background tints), hör hemma inuti (inside) BeginArtifact- och EndArtifact-inramningar så att den aldrig träder in i läsströmmen (reading stream). Språk är ärvbart (inheritable). Dokumentets standard (document default) kommer från SetPDFUAMode-argumentet, och ett flöde (run) på ett annat språk åsidosätter (overrides) det per element genom BeginTagEx eller SetStructElemLang. Det är vad som håller ett franskt citat inuti en engelsk manual uttalbart (pronounceable)

Före sparning kör GetPDFUADiagnostics bibliotekets strukturella kontroller över dokumentet i minnet (in-memory document) och returnerar (returns) fynd (findings) som text, där en tom sträng (empty string) betyder (means) att ingenting hittades (was found). Koderna namnger (name) de klassiska författar-misstagen direkt: FIGURE-NO-ALT för en bild utan alternativtext (alternate text), HEADING-LEVEL-SKIP för en H3:a som följer (following) efter en H1:a, ROLEMAP-UNMAPPED för en anpassad (custom) typ som aldrig registrerades. Koppla in det här (Wire this into) i bygget (build) – generera dokumentuppsättningen (document set), fäll (fail) steget vid icke-tom diagnostik – och tillgänglighets-regressioner blir fel av kompileringstids-karaktär (compile-time-style failures) i stället för (instead of) granskningsfynd (audit findings) månader senare. Den fullständiga (full) överensstämmelse-domen (conformance verdict) tillhör (belongs to) fortfarande preflight på den sparade (saved) filen, vilket täcks i (covered in) PDF/A- och PDF/UA-preflight (preflight) i Delphi, eftersom vissa normaliseringar (normalizations) appliceras först (only) under serialiseringen (serialization)

Annotations-navigering har ett eget vred (knob). PDF/UA förväntar sig tangentbords-traversering (keyboard traversal) av formulärfält (form fields) och länkar (links) för att följa (follow) struktur-ordningen, och SetTabOrderMode skriver tabulator-ordnings-posten (tab-order entry) på sidnivå som visare (viewers) respekterar (honor), med GetTabOrderMode tillgänglig (available) för att granska inkommande filer. Det är typen (kind) av krav (requirement) som ingen lägger märke till förrän (until) en enbart-tangentbord-användare (keyboard-only user) rapporterar (files) buggen (bug), och det kostar (costs) ett anrop (call) per dokument att få rätt (get right)

Strukturträd överlever inte varje sammanslagning (merge)

Taggade dokument förblir taggade endast (only) när varje senare process-steg (processing step) bevarar (preserves) trädet, och den vassa kanten (sharp edge) inuti PDFlibPas är sammanslagnings-list-familjen (merge-list family). MergeFileListFast byter bort (trades) bevarande (preservation) av strukturträd (structure-tree) mot (for) hastighet. Det är rätt byte (trade) för partier av skannade bilder (scanned image batches) och helt fel (wrong one) för taggade (tagged) rapporter, eftersom utdata öppnas fint (opens fine), renderar identiskt (identically) och i tysthet (quietly) har förlorat sitt tillgänglighets-lager (accessibility layer). Använd (Use) standard (default) MergeFileList eller den strikta (strict) varianten närhelst någon inmatning (input) är taggad, och gör (make) IsTaggedPDF till en del av påståendena efter montering (post-assembly assertions) så att ett tillplattat (flattened) parti inte kan skeppas utan att (without) någon märker det (noticing). Monterings-pipelines (Assembly pipelines) för stora dokument-uppsättningar (document sets) bär (carry) fler avvägningar av det här slaget (trade-offs of this kind), utforskade (explored) i sammanslagning, uppdelning och direktåtkomst av stora PDF-filer

Verifierings-loopen stängs (closes) utanför biblioteket: öppna utdata i Acrobat, inspektera tagg-panelen (tags panel) och läs (read) åtminstone ett dokument per mallfamilj (template family) med en riktig skärmläsare. Diagnostik (Diagnostics) fångar strukturella misstag (structural mistakes); endast (only) ett mänskligt öra fångar en läsordning (reading order) som är tekniskt giltig och praktiskt förbryllande (baffling). Utvärderings-byggen (Evaluation builds) och den fullständiga referensen för taggnings-API:et (tagging API reference) finns (are on) på produktsidan för losLab PDF Library för Delphi