Váš preflight označí súbor ako čistý podľa PDF/UA. veraPDF otvorí ten istý súbor a nahlási Figure bez alternatívneho textu podľa klauzuly 7.3. Oba nástroje majú pravdu a medzera medzi nimi presne vystihuje problém kontroly prístupnosti skenovaním bajtov. Byte-level prechod potvrdí, že súbor tvrdí, že je označkovaný: nájde /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part v XMP pakete, názov dokumentu a jazyk. To všetko sú formátové markery a sú potrebné. Nepovedia vám však nič o tom, či skutočný obrázok na štvrtej strane nesie popis, ktorý môže čítačka obrazovky prečítať nahlas. Odpoveď je v strome značiek a získate ju len tak, že strom prejdete
PDFium Component je natívna VCL PDF knižnica pre Delphi a C++Builder a jej ValidatePdfUa robí oba prechody. Byte-level prechod rieši formátové markery. Nad ním sedí prechod cez strom štruktúry, ktorý načíta živý označkovaný strom, prejde každý prvok a overí malú sadu pravidiel obsahu s vysokou mierou istoty, pri ktorých chýbajúci atribút znamená skutočnú chybu prístupnosti, nie iba štylistickú voľbu. Tento článok je o tomto druhom prechode: čo kontroluje, prečo je logika pravidiel pure function bez DLL pod sebou a kde sa zámerne zastaví
Prečo byte scan nedokáže uvidieť chýbajúci Alt
ISO 14289-1 (PDF/UA-1) je vrstva požiadaviek postavená nad ISO 32000. Niektoré z týchto požiadaviek sú štrukturálne a viditeľné priamo v surovom súbore: katalóg musí deklarovať strom štruktúry, viewer preferences musia nastaviť DisplayDocTitle, fonty musia byť vložené. Token scanner, ktorý odstráni telá streamov a porovná name tokeny s hranicami oddeľovačov, to všetko dokáže overiť a PDFium ValidatePdfUaCompliance to presne robí pre klauzuly ako 7.1, 7.18 a 7.21
Lenže "každý Figure má alternatívny text" nie je vlastnosť syntaxe súboru. Je to vlastnosť logickej štruktúry, teda stromu označkovaných prvkov, ktorý mapuje obsah na význam. Položka Alt pri Figure môže byť v slovníku štruktúrneho prvku, môže byť dodaná cez /ActualText span alebo môže pochádzať z custom type namapovaného cez role map. Nedá sa spoľahlivo nájsť grepovaním na /Alt v byte streame, pretože tento reťazec sa vyskytuje aj v nesúvisiacich kontextoch, môže byť komprimovaný v object streame a nič vám nepovie o tom, ku ktorému prvku štruktúry patrí. Poctivý spôsob, ako na túto otázku odpovedať, je pýtať sa priamo stromu štruktúry dokumentu, prvok po prvku, teda tej istej vrstvy, ktorú vyhodnocujú veraPDF a PAC. Presne na tom stojí sada Tier-1 kontrol v PDFium: byte scan pre formát, prechod stromom pre obsah
Čítanie živého stromu značiek
Východiskový materiál je TPdf.GetStructureElements (sprístupnený aj ako vlastnosť StructureElements), ktorá vracia TPdfStructureElements, teda ploché pole záznamov TPdfStructureElement v poradí dokumentu. Každý záznam je projekciou jedného prvku štruktúry cez accessor functions z PDFium a obsahuje polia, ktoré pravidlá prístupnosti skutočne potrebujú:
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 TypeName je to, na ktorom validator stojí. Pochádza z FPDF_StructElement_GetType, ktoré vracia štandardný typ štruktúry daného prvku, teda jeho názov /S, po tom, čo PDFium vyrieši role map. AlternateText pochádza z FPDF_StructElement_GetAltText, ActualText z FPDF_StructElement_GetActualText a ID z FPDF_StructElement_GetID. Keďže pole je ploché a usporiadané, validator dokáže uvažovať o celom dokumente naraz namiesto rekurzie, čo je dôležité pri tom jedinom pravidle, ktoré je globálne a nie viazané na jednotlivý prvok
Checker je pure function a je to zámer
Logika pravidiel nežije vo vnútri metódy, ktorá komunikuje s DLL. Je to samostatná, verejná pure function:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Berie ploché pole prvkov a vracia sadu issue hodnôt. Nevolá žiadnu PDFium function, neotvára dokument a nesiaha na žiadny global state. Toto oddelenie je zámerné a prináša dva jasné zisky. Prvým je testovateľnosť: v unit teste si môžete zostaviť syntetické pole TPdfStructureElements, napríklad Figure bez Alt, Formula, ktorej jediný prístupný text je v ActualText, alebo dve Note so zhodným ID, a overiť výslednú sadu bez toho, aby bolo pdfium.dll vôbec prítomné. Logika pravidiel sa overuje offline, zatiaľ čo prechod cez DLL sa overuje samostatne pomocou smoke testu na živom dokumente, ktorý sa preskočí, keď knižnica chýba
Druhým ziskom je jasné rozdelenie zodpovedností. TPdf.ValidatePdfUa vlastní neporiadnu časť, teda načítanie každej strany, vytiahnutie jej prvkov a ich akumuláciu, a potom odovzdá čisté pole do pure checker. "Získaj dáta" (DLL, side effects, lifetime) a "vyhodnoť pravidlá" (pure, deterministic) sa nikdy nezamotajú. Keď treba zmeniť pravidlo, upravujete funkciu bez I/O
Čo tieto tri pravidlá skutočne kontrolujú
Prechod cez strom štruktúry vyhadzuje tri issue hodnoty, pridané na koniec TPdfUaValidationIssues, aby enum zostal ABI-stable pre existujúcich volajúcich: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt a pvuaiNoteMissingId. Telo je dosť malé na to, aby sa dalo celé rozumne prejsť:
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;
Klauzula 7.3 sa týka figure: prvok Figure musí poskytnúť textovú alternatívu. Skoršia verzia tejto kontroly pozerala len na položku Alt, čím bola prísnejšia než referenčné validátory. PDF/UA akceptuje figure, ktorej prístupný text je dodaný cez ActualText namiesto Alt, pretože replacement text je platná alternatívna reprezentácia, takže pravidlo označí Figure len vtedy, keď sú obe hodnoty Alt aj ActualText prázdne. Klauzula 7.7 pokrýva formula a po rovnakej oprave používa identický test Alt-or-ActualText. Vzorka z conformance corpus, ktorá dávala Formula prístupný text iba cez ActualText, bola dovtedy falošne odmietaná, kým sa formula vetva nezosúladila s figure vetvou
Klauzula 7.9 je iného druhu. Note musí mať /ID a toto ID musí byť jedinečné v rámci celého dokumentu. Chýbajúce ID je zlyhanie viazané na konkrétny prvok. Duplicitné duplicitné ID je vzťah medzi dvoma prvkami, a práve preto je ploché pole dôležité: pre každú Note checker prechádza späť cez už videné prvky a označí kolíziu s ktoroukoľvek skoršou Note s rovnakým ID. Cena je zrejmé O(n²) nad počtom Note, čo je pri reálnych dokumentoch zanedbateľné a zároveň to udrží funkciu ako jediný čitateľný loop bez pomocného indexu, ktorý by bolo treba synchronizovať
Akumulácia naprieč stranami, aby bola jedinečnosť globálna
PDFium sprístupňuje prvky štruktúry po stranách, nie po dokumente, takže orchestration v ValidatePdfUa ich musí zozbierať ešte pred spustením pravidiel. Každú stranu prejde pomocou FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, nezávisle od toho, akú stranu má komponent práve otvorenú, a pridá prvky každej strany do jedného poľa. Až potom zavolá pure checker:
// 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áve akumulácia robí kontrolu jedinečnosti podľa 7.9 správnou. Dve Note na rôznych stranách môžu zdieľať rovnaké ID. Ak by ste validovali stranu po strane, kolíziu by ste nikdy nevideli, pretože sada prvkov každej strany vyzerá vnútorne konzistentne. Zostavenie jedného poľa pre celý dokument je jediný spôsob, ako sa duplicita stane viditeľnou. Stojí za zmienku aj ochranná podmienka na začiatku: prechod stromom sa spustí len vtedy, keď byte-level prechod nenahlási pvuaiMissingStructTreeRoot. Neoznačkovaný dokument nemá strom, ktorým by sa dalo prejsť, a už bol označený kvôli chýbajúcemu rootu štruktúry, takže načítania po stranách sa úplne preskočia. Hlboký prechod tak nič nestojí pri dokumentoch, ktoré z neho nemôžu mať úžitok
Konzervatívne po návrhu: radšej niečo prehliadnuť, než vyhlásiť planý poplach
Najdôležitejšou vlastnosťou tohto validatora je to, čo odmieta robiť. Porovnáva len štandardné názvy typu /S, ktoré FPDF_StructElement_GetType vracia priamo, teda Figure, Formula a Note. Dokument, ktorý definuje custom type a cez role map ho mapuje na Figure, môže podľa toho, ako PDFium typ vyrieši, hlásiť vlastný názov. V takom prípade ho checker nerozpozná a zostane ticho. Je to false negative a je to zamýšľané správanie. Návrhové pravidlo znie radšej nahlásiť menej, než vytvoriť false positive, pretože preflight nástroj, ktorý kričí aj pri konformných súboroch, naučí používateľov ignorovať ho, a ignorovaný validator je horší než žiadny. Dekoratívne obrázky žijú v artifact streame, nie v strome štruktúry, takže sa vôbec neobjavia ako Figure. Nedostanete teda hlásenie o "missing Alt" pri pozadí, ktoré je správne označené ako artifact
Aj preto je rozsah obmedzený na tri pravidlá. Vnáranie heading úrovní (klauzula 7.4), scope hlavičiek tabuliek (7.5) aj detekcia cyklov v role map (7.1) sú legitímne požiadavky PDF/UA, ale ich kvalitná kontrola potrebuje skutočnú analýzu grafu a atribútov. Naivná kontrola presne produkuje false positive, ktoré tento návrh zakazuje. PDF/UA napríklad povoľuje vzory headingov ako H1, H2, H3, H3, ktoré by jednoduché pravidlo "musí striktne rásť" nesprávne odmietlo. Tieto kontroly sú ponechané na dedikované conformance tools. Sada Tier-1 je podmnožina, pri ktorej je chýbajúci atribút jednoznačný
Hranica, povedaná úplne priamo
Predtým, než to zapojíte do release gate, oplatí sa poznať dve hranice. Po prvé, checker je len taký dobrý, ako dobré dáta dokáže PDFium prečítať z prvku štruktúry. Niekoľko súborov z conformance corpus, ktoré referenčné validátory akceptujú, používa mechanizmus alternatívneho textu, ktorý PDFium nesprístupňuje, takže FPDF_StructElement_GetAltText vracia prázdnu hodnotu, hoci súbor je v skutočnosti konformný. Pure checker potom na neúplných dátach "správne" nahlási chýbajúci Alt. Je to false positive, ktorého pôvod je v pokrytí accessorov DLL, nie v logike pravidiel. Uvoľniť pravidlo tak, aby pohltilo tieto prípady, by zároveň oslepilo kontrolu voči skutočným chybám, ktoré má zachytiť, preto sú tieto prípady zdokumentované ako známe obmedzenie PDFium a nie zametené pod koberec
Po druhé, toto je preflight, nie certifikácia. Tier-1 zachytáva chyby obsahu s vysokou mierou istoty, ktoré byte scan zo samotnej podstaty nemôže nájsť, a robí to bez planých poplachov. Plná zhoda s PDF/UA, vrátane významu headingov, štruktúry tabuliek a správneho reading order, však stále patrí úplnému validatoru a napokon aj ľudskému reviewerovi. Použite ValidatePdfUa na rýchle a lacné zastavenie zjavne chybných súborov vo vlastnom pipeline a potom nechajte posledné slovo na veraPDF alebo PAC. Ten istý prechod cez strom štruktúry je základom aj pre vytvorenie prístupného PDF readera v Delphi, kde strom značiek riadi reading order aj hovorený text, a dopĺňa prácu na úrovni metadát v článku o kontrole PDF annotations z Delphi
API pre strom štruktúry a ValidatePdfUa validator opísaný tu sa dodávajú spolu s PDFium Component pre Delphi a C++Builder (VCL) aj Lazarus/FPC (LCL). Produktová stránka odkazuje na plnú API referenciu vrátane kompletného rozloženia záznamu TPdfStructureElement a enumerácie issue hodnôt, na ktorých tieto kontroly stoja