Вашият preflight отчита файла като чист по PDF/UA. veraPDF отваря същия файл и маркира Figure без alternate text по clause 7.3. И двата инструмента са прави, а разликата между тях е целият проблем при проверка на accessibility чрез сканиране на байтове. Byte-level pass-ът потвърждава, че файлът казва че е tagged: намира /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part в XMP packet-а, document title-а, language-а. Това са format markers и те са необходими. Те не ви казват нищо за това дали действителната фигура на страница четири има описание, което screen reader може да прочете на глас. Отговорът на този въпрос живее в tag tree-то и за да го получите, трябва да обхождате tree-то
PDFium Component е нативна VCL PDF библиотека за Delphi и C++Builder, а неговият ValidatePdfUa прави и двата pass-а. Byte-level pass-ът се грижи за format marker-ите. Над него стои structure-tree pass, който зарежда живото tagged tree, обхожда всеки елемент и проверява малкия набор от high-confidence content правила, при които липсващият attribute означава реален accessibility дефект, а не stylistic preference. Тази статия е за този втори pass: какво проверява, защо rule логиката е pure function без DLL под нея и къде умишлено спира
Защо byte scan не може да види липсващ Alt
ISO 14289-1 (PDF/UA-1) е слой от изисквания върху ISO 32000. Някои от тях са structural и видими в raw файла: catalog-ът трябва да декларира structure tree, viewer preferences трябва да задават DisplayDocTitle, fonts трябва да са embedded. Token scanner, който изчиства stream bodies и търси name tokens с delimiter граници, може да провери всички тези неща, а PDFium's ValidatePdfUaCompliance прави точно това за clauses като 7.1, 7.18 и 7.21
Но „всеки Figure има alternate text“ не е свойство на синтаксиса на файла. То е свойство на logical structure - дървото от tagged elements, което свързва content и meaning. Alt entry-то на Figure може да стои в structure element dictionary-а, да бъде подадено през /ActualText span или да идва от role-mapped custom type. Не можете надеждно да го намерите, като grep-вате за /Alt в byte stream-а, защото този низ се появява в несвързани контексти, може да е compressed вътре в object stream и не ви казва нищо за това кои structure element му принадлежи. Честният начин да отговорите на въпроса е да питате собственото tag tree на документа, елемент по елемент, същата повърхност, която veraPDF и PAC оценяват. Това е линията, около която са изградени Tier-1 проверките на PDFium: byte scan за format, tree walk за content
Четене на живото tag tree
Raw материалът е TPdf.GetStructureElements (също излагано като StructureElements property-то), което връща TPdfStructureElements - flat array от TPdfStructureElement records в document order. Всеки запис е проекция на един structure element през accessor функциите на PDFium, с полетата, които accessibility правилата реално имат нужда:
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;
field е този, върху който validator-ът се завърта. Той идва от TypeName, което връща standard structure type-а на елемента - неговото FPDF_StructElement_GetType name - след като PDFium е разрешил role map-а. /S идва от AlternateText, FPDF_StructElement_GetAltText от ActualText, и FPDF_StructElement_GetActualText от ID. Понеже array-ът е плосък и подреден, validator-ът може да разсъждава върху целия документ наведнъж, вместо да рекурсва - което е важно за единственото правило, което е global, а не по елемент.FPDF_StructElement_GetID. Because the array is flat and ordered, the validator can reason about the whole document at once instead of recursing — which matters for the one rule that is global rather than per-element
Самата checker логика е pure function и това е нарочно
Rule логиката не живее в метода, който говори с DLL-а. Тя е самостоятелна, public, pure function:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Тя приема плосък element array и връща set от issue-и. Не извиква PDFium функция, не отваря документ, не пипа global state. Това разделение е умишлено и носи полза два пъти. Първо, testability: можете да построите synthetic TPdfStructureElements array в unit test - Figure без Alt, Formula, чийто единствен accessible текст е в ActualText, две Note, които споделят ID - и да assert-нете върху result set-а без pdfium.dll изобщо да присъства. Rule логиката се проверява offline; DLL traversal-ът се проверява отделно от live-document smoke test, който се пропуска, когато библиотеката липсва
Второ, яснота на отговорността. TPdf.ValidatePdfUa притежава мръсната част - зареждането на всяка страница, изваждането на елементите ѝ, натрупването им - и после подава чист array на pure checker-а. „Вземи данните“ (DLL, side effects, lifetime) и „съди правилата“ (pure, deterministic) никога не се заплитат. Когато правило трябва да се промени, променяте функция без I/O вътре в нея
Какво реално проверяват трите правила
Structure-tree pass-ът връща три issue values, добавени в края на TPdfUaValidationIssues така че enum-ът да остане ABI-stable за съществуващите callers: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, и pvuaiNoteMissingId. Body-то е достатъчно малко, за да може да се разсъждава върху него изцяло:
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;
Clause 7.3 управлява фигурите: Figure element трябва да предоставя text alternative. Ранната версия на тази проверка гледаше само Alt entry-то, което я правеше по-строга от reference validator-ите. PDF/UA приема и фигура, чийто accessible текст идва чрез ActualText вместо това - replacement text е валидно алтернативно представяне - така че правилото маркира Figure само когато и двете Alt и ActualText са празни. Clause 7.7 покрива формулите и след същата корекция използва идентичния Alt-or-ActualText test; sample от conformance corpus, който даваше на Formula своя accessible текст само чрез ActualText, беше отхвърлян фалшиво, докато Formula клонът не беше приведен в съответствие с Figure клона
Clause 7.9 е различен по вид. Note трябва да има /ID, и този ID трябва да е уникален за целия документ. Липсващ ID е per-element failure. duplicate ID е връзка между два елемента, което е причината плоският array да има значение: за всяка Note checker-ът сканира назад през вече видените елементи и маркира collision с всяка по-ранна Note, която носи същия ID. Цената е очевидното O(n²) спрямо броя Note-и, което е без значение за всеки реален документ и запазва функцията като един четим loop без помощен index за синхронизация
Натрупване през страници, така че уникалността да е глобална
PDFium излага структура elements по страница, не по документ, така че orchestration-ът в ValidatePdfUa трябва да ги събере преди да тръгнат правилата. Той обхожда всяка страница с FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, независимо коя страница компонентът в момента има отворена, и добавя елементите на всяка страница в един array. Едва тогава вика 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;
Натрупването е това, което прави 7.9 uniqueness проверката правилна. Две Notes на различни страници могат да споделят ID; ако валидирате страница по страница, никога няма да видите collision-а, защото set-ът от елементи на всяка страница изглежда вътрешно последователен. Изграждането на един документ-широк array е единственият начин duplicate-ът да стане видим. Guard-ът отпред също си струва да се отбележи: tree walk-ът се стартира само когато byte-level pass-ът не е отчел pvuaiMissingStructTreeRoot. Untagged документ няма tree за обхождане и вече е бил маркиран за липсващ structure root, така че per-page load-овете се пропускат изцяло. Deep pass-ът не струва нищо на документите, които не могат да спечелят от него
Консервативно по дизайн: пропускай тихо, никога не викай вълк
The single most important property of this validator is what it refuses to do. It matches only the standard /S type names that FPDF_StructElement_GetType returns directly — Figure, Formula, Note. A document that defines a custom type and role-maps it to Figure will, depending on how PDFium resolves the type, report its own name. When that happens the checker does not recognize it and stays silent. That is a false negative, and it is the intended behavior. The design rule is to под-отчита, вместо някога да произвежда false positive, защото preflight инструмент, който вика вълк върху conformant файлове, учи потребителите си да го игнорират - а игнориран validator е по-лош от никакъв. Декоративните изображения живеят в artifact stream-а, не в structure tree-то, така че изобщо не се появяват като Figures; няма да получите оплакване за "missing Alt" за background rule, която правилно е маркирана като artifact
Това е и причината обхватът да е ограничен до три правила. Heading-level nesting (clause 7.4), table header scope (7.5) и role-map cycle detection (7.1) са напълно легитимни PDF/UA изисквания, но проверката им добре изисква истински graph и attribute analysis, а проверката им наивно произвежда точно false positive-ите, които design-ът забранява - PDF/UA позволява heading patterns като H1, H2, H3, H3, които едно просто правило „трябва строго да растат“ би отхвърлило погрешно. Тези проверки се оставят на dedicated conformance инструменти. Tier-1 наборът е подмножеството, при което липсващият attribute е недвусмислен
Границата, казана ясно
Две ограничения си струва да знаете, преди да вържете това към release gate. Първо, checker-ът е толкова добър, колкото това, което PDFium може да прочете от structure element-а. Няколко файла от conformance corpus-а, които reference validator-ите приемат, използват alternate-text механизъм, който PDFium не излага, така че FPDF_StructElement_GetAltText връща empty, въпреки че файлът наистина е conformant. Pure checker-ът тогава „правилно“ маркира missing Alt върху непълни данни - false positive, който произхожда от coverage-а на DLL accessors, а не от rule логиката. Да разхлабите правилото, за да погълне тези случаи, би го направило сляпо за реалните failure-и, които трябва да хваща, така че те се документират като known PDFium limitation, вместо да се замазват
Второ, това е preflight, не certification. Tier-1 хваща high-confidence content грешките, които byte scan structural не може да хване, и го прави без false alarms - но пълното PDF/UA съответствие, включително heading semantics, table structure и reading-order correctness, все още принадлежи на complete validator и в крайна сметка на human reviewer. Използвайте ValidatePdfUa за да проваляте очевидните дефекти бързо и евтино в собствения си pipeline, а после оставете veraPDF или PAC да дадат последната дума. Същото structure-tree traversal стои и зад изграждането на accessible PDF reader in Delphi, където tag tree-то управлява reading order и spoken text, и допълва работата на metadata-level при reviewing PDF annotations from Delphi
Structure-tree API-тата и ValidatePdfUa validator-ът, показани тук, идват с PDFium Component за Delphi и C++Builder (VCL) и Lazarus/FPC (LCL). Product page-ът линква пълната API reference, включително пълния TPdfStructureElement record layout и issue enumeration-а зад тези проверки