Teknisk artikkel

Validering av PDF/UA-taggtreet i Delphi med PDFium

Preflighten din rapporterer at filen er PDF/UA-ren. veraPDF åpner den samme filen og flagger en Figure uten alternativ tekst under klausul 7.3. Begge verktøyene har rett, og gapet mellom dem er selve problemet med å kontrollere tilgjengelighet ved å skanne byte for byte. En bytenivåpass bekrefter at filen sier at den er tagget: den finner /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part i XMP-pakken, dokumenttittelen og språket. Dette er formatmarkører, og de er nødvendige. De sier ingenting om hvorvidt den faktiske figuren på side fire har en beskrivelse som en skjermleser kan lese høyt. Svaret ligger i taggtreet, og for å få det må du gå gjennom treet

PDFium Component er et innebygd VCL PDF-bibliotek for Delphi og C++Builder, og ValidatePdfUa gjør begge passene. Bytenivåpasset håndterer formatmarkørene. Oppå det ligger et strukturtrepass som laster det levende taggede treet, går gjennom hvert element og sjekker det lille settet med innholdskrav med høy sikkerhet der en manglende egenskap betyr en reell tilgjengelighetsfeil i stedet for en stilistisk preferanse. Denne artikkelen handler om det andre passet: hva det sjekker, hvorfor regellogikken er en ren funksjon uten DLL under seg, og hvor den bevisst stopper

Hvorfor en byteskanning ikke kan se en manglende Alt

ISO 14289-1 (PDF/UA-1) er et lag med krav oppå ISO 32000. Noen av disse kravene er strukturelle og synlige i råfilen: katalogen må deklarere et strukturtre, visningsinnstillingene må sette DisplayDocTitle, fonter må være innebygd. En tokenskanner som fjerner strøminnhold og matcher navnetokens med avgrensningsgrenser kan verifisere alt dette, og PDFiums ValidatePdfUaCompliance gjør akkurat det for bestemmelser som 7.1, 7.18 og 7.21

Men at "hver Figure har alternativ tekst" er ikke en egenskap ved filens syntaks. Det er en egenskap ved den logiske strukturen - treet av taggede elementer som knytter innhold til mening. En Figures Alt-oppføring kan ligge i struktur-elementets ordbok, leveres gjennom et /ActualText-spenn, eller komme fra en rolle-mappet tilpasset type. Du kan ikke pålitelig finne den ved å søke etter /Alt i bytestrømmen, fordi den strengen også finnes i andre sammenhenger, kan være komprimert inne i en objektstrøm, og ikke sier noe om hvilket struktur-element den tilhører. Den ærlige måten å svare på spørsmålet er å spørre dokumentets eget strukturtre, element for element, den samme overflaten veraPDF og PAC vurderer. Det er denne linjen PDFiums Tier-1-sjekker er bygget rundt: byteskanning for format, trevandring for innhold

Lese det levende taggtreet

Råmaterialet er TPdf.GetStructureElements (også eksponert som StructureElements-egenskapen), som returnerer en TPdfStructureElements - en flat matrise av TPdfStructureElement-poster i dokumentrekkefølge. Hver post er projeksjonen av ett struktur-element gjennom PDFiums tilgangsfunksjoner, med feltene tilgjengelighetsreglene faktisk trenger:

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Feltet TypeName er det validatoren styrer etter. Det kommer fra FPDF_StructElement_GetType, som returnerer elementets standard strukturtype - dets /S-navn - etter at PDFium har løst rolle-mappen. AlternateText kommer fra FPDF_StructElement_GetAltText, ActualText fra FPDF_StructElement_GetActualText, og ID fra FPDF_StructElement_GetID. Fordi matrisen er flat og ordnet, kan validatoren resonnere om hele dokumentet på én gang i stedet for å rekursere - noe som betyr noe for den ene regelen som er global i stedet for per element

Validatoren er en ren funksjon, og det er med vilje

Regellogikken ligger ikke inne i metoden som snakker med DLL-en. Den er en selvstendig, offentlig, ren funksjon:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Den tar en flat elementmatrise og returnerer et sett med avvik. Den kaller ingen PDFium-funksjon, åpner ikke noe dokument og berører ingen global tilstand. Den adskillelsen er bevisst, og den lønner seg to ganger. Først, testbarhet: du kan bygge en syntetisk TPdfStructureElements-matrise i en enhetstest - en Figure uten Alt, en Formula der den eneste tilgjengelige teksten ligger i ActualText, to Notes som deler samme ID - og hevde på resultatsettet uten pdfium.dll til stede i det hele tatt. Regellogikken verifiseres offline; DLL-gjennomgangen verifiseres separat av en smoke-test mot et levende dokument som hoppes over når biblioteket mangler

For det andre, tydelig ansvarsfordeling. TPdf.ValidatePdfUa eier den rotete delen - laster hver side, henter elementene deres, akkumulerer dem - og gir deretter en ren matrise til den rene sjekken. "Hent dataene" (DLL, sideeffekter, levetid) og "vurder reglene" (ren, deterministisk) blandes aldri. Når en regel må endres, endrer du en funksjon som ikke har noe I/O i seg

Hva de tre reglene faktisk sjekker

Strukturtrepasset legger til tre avvikverdier, lagt til på slutten av TPdfUaValidationIssues slik at enum-en forblir ABI-stabil for eksisterende kallere: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt og pvuaiNoteMissingId. Kjernen er liten nok til at den kan forstås fullt ut:

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

Klausul 7.3 gjelder figurer: et Figure-element må gi et tekstalternativ. Den tidlige versjonen av denne sjekken så bare på Alt-oppføringen, noe som gjorde den strengere enn referansevalidatorene. PDF/UA godtar en figur der den tilgjengelige teksten leveres gjennom ActualText i stedet - erstatningstekst er en gyldig alternativ representasjon - så regelen flagger bare en Figure når både Alt og ActualText er tomme. Klausul 7.7 dekker formler, og etter samme korrigering bruker den den identiske Alt-eller-ActualText-testen; et samsvarsutvalg som ga en Formula sin tilgjengelige tekst bare gjennom ActualText ble feilaktig avvist helt til Formula-grenen ble brakt i linje med Figure-grenen

Klausul 7.9 er annerledes i sin natur. En Note må ha en /ID, og den ID-en må være unik på tvers av dokumentet. En manglende ID er en feil per element. En duplikat-ID er et forhold mellom to elementer, og derfor betyr den flate matrisen noe: for hver Note skanner sjekken bakover over elementene som allerede er sett og flagger en kollisjon med enhver tidligere Note som bærer samme ID. Kostnaden er den åpenbare O(n²) over antall Notes, noe som er irrelevant for ethvert reelt dokument og holder funksjonen til én lesbar løkke uten en ekstra indeks som må holdes synkron

Akkumulering på tvers av sider gjør unikhet global

PDFium eksponerer struktur-elementer per side, ikke per dokument, så orkestreringen i ValidatePdfUa må samle dem før reglene kjøres. Den går gjennom hver side med FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, uavhengig av hvilken side komponenten for øyeblikket har åpen, og legger alle sidens elementer inn i én matrise. Først da kaller den den rene sjekken:

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

Akkumuleringen er det som gjør 7.9-unikhetssjekken korrekt. To Notes på forskjellige sider kan dele ID; hvis du validerte side for side ville du aldri se kollisjonen, fordi hvert sides elementsett ser internt konsistent ut. Å bygge én dokumentomfattende matrise er den eneste måten du får duplikatet til å bli synlig. Verdt å merke seg er også sperren foran: trevandringen kjøres bare når bytenivåpasset ikke rapporterte pvuaiMissingStructTreeRoot. Et dokument uten tagger har intet tre å gå gjennom og er allerede flagget for manglende strukturrot, så sidelastingene hoppes helt over. Dyp-passeringen koster ingenting på dokumentene som ikke kan få nytte av den

Konservativ av design: heller bomme stille enn å rope ulv

Den aller viktigste egenskapen ved denne validatoren er det den nekter å gjøre. Den matcher bare de standard /S-typenavnene som FPDF_StructElement_GetType returnerer direkte - Figure, Formula, Note. Et dokument som definerer en tilpasset type og rolle-mapper den til Figure vil, avhengig av hvordan PDFium løser typen, rapportere sitt eget navn. Når det skjer, kjenner ikke sjekken den igjen og forblir stille. Det er en falsk negativ, og det er den tiltenkte oppførselen. Designregelen er å under-rapportere heller enn noen gang å produsere en falsk positiv, fordi et preflight-verktøy som roper ulv på samsvarende filer lærer brukerne å ignorere det - og en ignorert validator er verre enn ingen. Dekorative bilder lever i artifact-strømmen, ikke i strukturtreet, så de dukker ikke opp som Figures i utgangspunktet; du får ingen "manglende Alt"-klage på en bakgrunnsregel som er korrekt markert som en artifact

Dette er også grunnen til at omfanget holdes til tre regler. Nesting av overskriftsnivåer (klausul 7.4), tabellhodets omfang (7.5) og oppdagelse av role-map-sykler (7.1) er alle legitime PDF/UA-krav, men å sjekke dem godt krever ekte graf- og attributtanalysen, og å sjekke dem naivt produserer akkurat de falske positive som designet forbyr - PDF/UA tillater overskriftsmønstre som H1, H2, H3, H3 som en enkel "må strengt øke"-regel ville avvise feilaktig. Disse sjekkene overlates til dedikerte samsvaringsverktøy. Tier-1-settet er delmengden der en manglende egenskap er entydig

Grensen, sagt rett ut

To begrensninger er verdt å kjenne før du kobler dette inn i en release-gate. For det første er sjekken bare så god som det PDFium kan lese fra struktur-elementet. En håndfull filer i conformance-corpus som referansevalidatorene passerer bruker en alternativtekst-mekanisme PDFium ikke eksponerer, så FPDF_StructElement_GetAltText returnerer tomt selv om filen faktisk er konform. Den rene sjekken flagger da "korrekt" en manglende Alt på ufullstendige data - en falsk positiv som stammer fra DLL-ens tilgangsdekning, ikke fra regellogikken. Å løsne regelen for å absorbere disse tilfellene ville også gjøre den blind for de reelle feilene den skal fange, så de dokumenteres som en kjent PDFium-begrensning i stedet for å bli skjult

For det andre er dette en preflight, ikke en sertifisering. Tier-1 fanger de innholdfeilene med høy sikkerhet som en byteskanning strukturelt ikke kan, og den gjør det uten falske alarmer - men full PDF/UA-samsvar, inkludert semantikk for overskrifter, tabellstruktur og korrekt leserekkefølge, hører fortsatt hjemme hos en komplett validator og til syvende og sist hos en menneskelig gjennomleser. Bruk ValidatePdfUa til å stoppe de åpenbare feilene raskt og billig i din egen pipeline, og la deretter veraPDF eller PAC få siste ord. Den samme strukturtrevandringen ligger under byggingen av en tilgjengelig PDF-leser i Delphi, der taggtreet styrer leserekkefølge og opplest tekst, og den utfyller metadataarbeidet i gjennomgang av PDF-annotasjoner i Delphi

API-ene for strukturtreet og ValidatePdfUa-validatoren som vises her leveres med PDFium Component for Delphi og C++Builder (VCL) og Lazarus/FPC (LCL). Produktsiden lenker til hele API-referansen, inkludert den komplette TPdfStructureElement-poststrukturen og avviksenumerasjonen bak disse sjekkene