Technický článek

Validace stromu struktury PDF/UA v Delphi s PDFium

Váš preflight hlásí, že je soubor čistý z hlediska PDF/UA. veraPDF otevře tentýž soubor a označí Figure bez alternativního textu podle článku 7.3. Oba nástroje mají pravdu a rozdíl mezi nimi přesně vystihuje problém kontroly přístupnosti pouhým procházením bajtů. Průchod na úrovni bajtů potvrdí, že soubor tvrdí že je tagovaný: najde /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part v paketu XMP, název dokumentu i jazyk. To jsou značky formátu a jsou nezbytné. Nic vám ale neřeknou o tom, zda skutečný obrázek na čtvrté stránce nese popis, který může odečítač obrazovky přečíst nahlas. Odpověď je ve stromu tagů a abyste ji získali, musíte strom projít

PDFium Component je nativní VCL PDF knihovna pro Delphi a C++Builder a jeho ValidatePdfUa zvládá oba průchody. Průchod na úrovni bajtů obsluhuje značky formátu. Nad ním stojí průchod stromem struktury, který načte živý tagovaný strom, projde každý prvek a zkontroluje malou sadu vysoce spolehlivých pravidel obsahu, u nichž chybějící atribut znamená skutečnou chybu přístupnosti, nikoli stylistickou preferenci. Tento článek je o tom druhém průchodu: co kontroluje, proč je logika pravidel čistá funkce bez DLL pod ní a kde záměrně končí

Proč kontrola bajtů neodhalí chybějící Alt

ISO 14289-1 (PDF/UA-1) je vrstva požadavků nad ISO 32000. Některé z těchto požadavků jsou strukturální a viditelné v syrovém souboru: katalog musí deklarovat strom struktury, předvolby zobrazení musí nastavit DisplayDocTitle, písma musí být vložená. Kontrola tokenů, která odstraňuje těla streamů a porovnává tokeny názvů s hranicemi oddělovačů, to všechno ověří, a PDFiumova kontrola bajtů ValidatePdfUaCompliancedělá přesně to pro ustanovení jako 7.1, 7.18 a 7.21

Ale tvrzení "každý Figure má alternativní text" není vlastnost syntaxe souboru. Je to vlastnost logické struktury - stromu tagovaných prvků, který mapuje obsah na význam. Záznam Alt u Figure může být ve slovníku strukturního prvku, může být poskytnut prostřednictvím /ActualText span, nebo může pocházet z vlastního typu namapovaného na roli. Nelze jej spolehlivě najít hledáním řetězce /Alt v toku bajtů, protože se tento řetězec objevuje v nesouvisejících souvislostech, může být komprimován v objektovém streamu a neříká nic o tom, kterému strukturnímu prvku patří. Poctivý způsob, jak na tu otázku odpovědět, je ptát se vlastního stromu struktury dokumentu, prvek po prvku, přesně tak, jak to hodnotí veraPDF a PAC. Na tom stojí kontroly PDFium Tier-1: kontrola bajtů pro formát, průchod stromem pro obsah

Práce se živým stromem tagů

Základem je TPdf.GetStructureElements (také dostupné jako StructureElementsvlastnost), která vrací TPdfStructureElements - ploché pole TPdfStructureElementzáznamů v pořadí dokumentu. Každý záznam je projekce jednoho strukturního prvku přes přístupové funkce PDFium a obsahuje pole, která pravidla přístupnosti skutečně potřebují:

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;

Pole TypeNameje to, na které se validátor soustředí. Pochází z FPDF_StructElement_GetType, která vrací standardní typ struktury prvku - jeho /Snázev - poté, co PDFium vyřeší mapu rolí. AlternateTextpochází z FPDF_StructElement_GetAltText, ActualTextz FPDF_StructElement_GetActualText, a IDz FPDF_StructElement_GetID. Protože je pole ploché a seřazené, může validátor uvažovat nad celým dokumentem najednou místo rekurze - a to je důležité pro jediné pravidlo, které je globální, ne per-prvek

Kontrola je čistá funkce a je to záměr

Logika pravidla neleží uvnitř metody, která mluví s DLL. Je to samostatná, veřejná, čistá funkce:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Přijímá ploché pole prvků a vrací sadu problémů. Nepovolává žádnou funkci PDFium, neotvírá žádný dokument, nedotýká se žádného globálního stavu. Toto oddělení je záměrné a vyplácí se dvakrát. Zaprvé testovatelnost: v unit testu můžete sestavit syntetické TPdfStructureElements pole s prvky - Figure bez Alt, Formula, jejíž jediný přístupný text je v ActualText, dvě Notes, které sdílejí jedno ID - a ověřit výsledek bez pdfium.dll přítomného vůbec. Logika pravidla se ověřuje offline; průchod DLL se ověřuje zvlášť pomocí smoke testu s živým dokumentem, který se přeskočí, když knihovna chybí

Za druhé, jasné rozdělení odpovědnosti. TPdf.ValidatePdfUa nese tu nepořádnou část - načítá každou stránku, získává z ní prvky, hromadí je - a pak předá čisté pole čisté kontrole. "Získej data" (DLL, vedlejší efekty, životnost) a "posuď pravidla" (čisté, deterministické) se nikdy nespletou. Když je třeba pravidlo změnit, měníte funkci, která v sobě nemá žádné I/O

Co vlastně kontrolují tři pravidla

Průchod strukturálním stromem vyvolá tři hodnoty problémů, připojené na konec TPdfUaValidationIssues aby výčet zůstal ABI-stabilní pro stávající volající: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, a pvuaiNoteMissingId. Tělo je dost malé na to, aby bylo možné mu úplně porozumět:

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;

Bod 7.3 se týká obrázků: Figure prvek musí poskytnout textovou alternativu. První verze této kontroly se dívala jen na položku Alt, což ji činilo přísnější než referenční validátory. PDF/UA přijímá obrázek, jehož přístupný text je poskytnut přes ActualText místo toho - náhradní text je platná alternativní reprezentace - takže pravidlo označí Figure jen tehdy, když obě Alt a ActualText jsou prázdné. Bod 7.7 se týká formulí a po stejné opravě používá totožný test Alt-or-ActualText; vzorek z validačního korpusu, který dával Formuli její přístupný text pouze přes ActualText, byl neprávem zamítán, dokud nebyla větev pro Formula uvedena do souladu s větví pro Figure

Bod 7.9 je jiný svého druhu. Note musí mít /ID, a toto ID musí být v celém dokumentu jedinečné. Chybějící ID je selhání na úrovni prvku. duplicitní ID je vztah mezi dvěma prvky, proto záleží na plochém poli: pro každé Note kontrola prochází zpět přes již viděné prvky a označí kolizi s jakýmkoli dřívějším Note se stejným ID. Cena je zřejmé O(n²) vzhledem k počtu Note, což je pro jakýkoli reálný dokument bezvýznamné a nechává funkci jako jedinou čitelnou smyčku bez pomocného indexu, který by bylo třeba udržovat v synchronizaci

Shromažďování napříč stránkami, aby byla jedinečnost globální

PDFium zpřístupňuje strukturální prvky po stránkách, ne po dokumentu, takže orchestrace v ValidatePdfUa je musí před spuštěním pravidel nasbírat. Prochází každou stránku pomocí FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, nezávisle na tom, jaká stránka je právě otevřená v komponentě, a přidá všechny prvky ze všech stránek do jednoho pole. Teprve potom zavolá čistou kontrolu:

// 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;

Právě toto shromažďování dělá kontrolu jedinečnosti v 7.9 správnou. Dvě Notes na různých stránkách mohou sdílet stejné ID; kdybyste validovali stránku po stránce, kolizi byste nikdy neviděli, protože sada prvků na každé stránce vypadá vnitřně konzistentně. Sestavení jediného pole pro celý dokument je jediný způsob, jak se duplicita stane viditelnou. Stojí za zmínku i ochrana na začátku: průchod stromem běží jen tehdy, když bajtový průchod nenahlásil pvuaiMissingStructTreeRoot. Dokument bez tagů nemá žádný strom k procházení a už byl označen za chybějící structure root, takže načítání po stránkách se úplně přeskočí. Hluboký průchod nestojí na dokumentech, ze kterých nemůže těžit, nic

Konzervativní záměr: raději minout potichu než planě varovat

Nejdůležitější vlastností tohoto validátoru je to, co odmítá dělat. Odpovídá jen standardním /S názvům typů, které FPDF_StructElement_GetType vrací přímo - Figure, Formula, Note. Dokument, který definuje vlastní typ a namapuje ho pomocí mapování rolí na Figure, bude v závislosti na tom, jak PDFium typ vyřeší, hlásit vlastní název. Když se to stane, kontrola jej nepozná a zůstane potichu. To je falešně negativní výsledek a je to zamýšlené chování. Návrhové pravidlo zní under-report rather than ever produce a false positiveraději podhlásit, než kdy vytvořit falešně pozitivní nález

, protože nástroj pro předběžnou kontrolu, který na vyhovujících souborech křičí "vlk", si své uživatele naučí ho ignorovat, a ignorovaný validátor je horší než žádný. Dekorativní obrázky patří do proudu artefaktů, ne do stromu struktury, takže se vůbec nikdy neobjeví jako Figures; na pozadí správně označeném jako artefakt se tedy hláška o chybějícím Alt neobjeví

To je také důvod, proč se rozsah drží tří pravidel. Vnořování úrovní nadpisů (klauzule 7.4), rozsah záhlaví tabulek (7.5) a detekce cyklů v role mapě (7.1) jsou všechny legitimní požadavky PDF/UA, ale jejich spolehlivá kontrola vyžaduje skutečnou analýzu grafu a atributů a jejich naivní kontrola vytváří přesně ty falešně pozitivní nálezy, které návrh zakazuje. PDF/UA například připouští posloupnosti nadpisů jako H1, H2, H3, H3, které by jednoduché pravidlo "musí se přísně zvyšovat" chybně odmítlo. Tyto kontroly patří do specializovaných nástrojů pro shodu s normou. Sada Tier-1 je podmnožina, u níž je chybějící atribut jednoznačný

Hranice, řečeno přímoFPDF_StructElement_GetAltTextPřed zapojením do vydávací brány stojí za to znát dvě omezení. Za prvé, kontrola je jen tak dobrá, jak dobře PDFium dokáže z prvku struktury přečíst data. Několik souborů z korpusu shody, které referenční validátory projdou, používá mechanismus alternativního textu, který PDFium nezpřístupňuje, takže

vrací prázdnou hodnotu, i když je soubor ve skutečnosti v souladu s normou. Samotný checker pak na neúplných datech "správně" nahlásí chybějící Alt, což je falešně pozitivní nález, který pramení z pokrytí přístupových metod v DLL, ne z logiky pravidla. Uvolnění pravidla, aby takové případy pohltilo, by zároveň oslepilo vůči skutečným chybám, které má zachytit, takže jsou zdokumentovány jako známé omezení PDFium místo toho, aby byly zameteny pod koberec.ValidatePdfUaZa druhé, jde o předběžnou kontrolu, ne o certifikaci. Tier-1 zachytí vysoce jisté obsahové chyby, které čistý byte scan strukturálně zachytit nemůže, a dělá to bez falešných poplachů, ale plná shoda s PDF/UA, včetně sémantiky nadpisů, struktury tabulek a správnosti pořadí čtení, stále patří kompletnímu validátoru a nakonec i lidskému kontrolorovi. Použijte k rychlému a levnému selhání zjevných vad ve vlastním řetězci, a pak nechte veraPDF nebo PAC říct poslední slovo. Stejné procházení stromu struktury je základem i pro sestavení přístupné čtečky PDF v Delphi, kde strom tagů určuje pořadí čtení i mluvený text, a doplňuje práci na úrovni metadat v kontrole PDF anotací v Delphi

.ValidatePdfUaRozhraní API pro strom struktury a zde uvedený validátor se dodávají s PDFium ComponentTPdfStructureElement pro Delphi a C++Builder (VCL) i Lazarus/FPC (LCL). Stránka produktu odkazuje na úplnou referenci API, včetně kompletního