사전검사 결과는 파일이 PDF/UA clean 하다고 말합니다. 그런데 veraPDF 로 같은 파일을 열어 보면 clause 7.3 아래에서 alternate text 가 없는 Figure 를 지적합니다. 두 도구 모두 맞고, 이 간극 자체가 바이트 스캔만으로 접근성을 검사할 때의 본질적인 문제입니다. 바이트 수준 패스는 파일이 tagged 라고 말하고 있는지 는 확인할 수 있습니다. /StructTreeRoot, /MarkInfo /Marked true, XMP packet 안의 pdfuaid:part, 문서 제목, 언어를 찾을 수 있습니다. 이것들은 형식 marker 이며 반드시 필요합니다. 하지만 4페이지의 실제 figure 에 screen reader 가 읽을 수 있는 설명이 붙어 있는지는 알려주지 못합니다. 그 답은 tag tree 안에 있으므로, 얻으려면 트리를 직접 걸어야 합니다
PDFium Component 는 Delphi 와 C++Builder 용 native VCL PDF 라이브러리이며, 그 ValidatePdfUa 는 두 패스를 모두 수행합니다. byte-level 패스는 형식 marker 를 처리하고, 그 위에 structure-tree 패스가 얹혀 live tagged tree 를 로드해 모든 element 를 순회하며, 속성이 빠지면 스타일 차이가 아니라 실제 접근성 결함이라고 볼 수 있는 소수의 high-confidence 규칙만 검사합니다. 이 글은 그 두 번째 패스에 대한 설명입니다. 무엇을 검사하는지, 왜 rule logic 이 DLL 아래가 아니라 순수 함수로 분리되어 있는지, 그리고 어디에서 의도적으로 멈추는지를 다룹니다
왜 byte scan 으로는 빠진 Alt 를 볼 수 없는가
ISO 14289-1(PDF/UA-1)은 ISO 32000 위에 요구사항을 얹는 규격입니다. 그중 일부는 구조적이어서 raw file 에서도 보입니다. catalog 는 structure tree 를 선언해야 하고, viewer preference 는 DisplayDocTitle 를 설정해야 하며, font 는 임베드되어야 합니다. stream 본문을 비우고 delimiter 경계로 name token 을 맞추는 token scanner 라면 이런 항목을 검증할 수 있고, PDFium 의 ValidatePdfUaCompliance 는 7.1, 7.18, 7.21 같은 조항에서 정확히 그렇게 동작합니다
하지만 "모든 Figure 에 alternate text 가 있다"는 것은 파일 문법의 속성이 아닙니다. 그것은 논리 구조, 즉 콘텐츠를 의미에 매핑하는 tagged element tree 의 속성입니다. Figure 의 Alt entry 는 structure element dictionary 안에 있을 수도 있고, /ActualText span 으로 공급될 수도 있고, role-mapped custom type 에서 올 수도 있습니다. 바이트 stream 에서 /Alt 를 grep 해서 이것을 신뢰성 있게 판단할 수는 없습니다. 그 문자열은 무관한 맥락에도 등장하고, object stream 안에 압축돼 있을 수도 있으며, 무엇보다 그것이 어느 structure element 에 속하는지도 알 수 없습니다. 정직한 방식은 문서 자신의 structure tree 에 element 단위로 묻는 것입니다. veraPDF 와 PAC 가 평가하는 것과 같은 면을 보는 셈입니다. PDFium 의 Tier-1 검사는 이 선을 기준으로 설계됩니다. 형식은 byte scan, 콘텐츠는 tree walk 입니다
live tag tree 읽기
원재료는 TPdf.GetStructureElements 입니다. StructureElements property 로도 노출되며, 문서 순서대로 정렬된 TPdfStructureElements 를 반환합니다. 이것은 TPdfStructureElement record 들의 flat array 이며, 각 record 는 PDFium accessor function 을 통해 얻은 structure element 하나의 투영본으로서 접근성 규칙이 실제로 필요로 하는 필드만 담고 있습니다
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;
이 TypeName 필드가 validator 가 축을 잡는 부분입니다. 이것은 FPDF_StructElement_GetType 에서 오며, PDFium 이 role map 을 해석한 뒤의 element 표준 구조 타입, 즉 /S name 을 반환합니다. AlternateText 는 FPDF_StructElement_GetAltText, ActualText 는 FPDF_StructElement_GetActualText, ID 는 FPDF_StructElement_GetID 에서 옵니다. 배열이 flat 하고 ordered 되어 있기 때문에 validator 는 문서 전체를 한 번에 추론할 수 있고, 이 점은 per-element 규칙이 아니라 문서 전역 규칙을 다룰 때 특히 중요합니다
checker 는 순수 함수이며, 그것이 의도다
rule logic 은 DLL 과 통신하는 메서드 안에 들어 있지 않습니다. 독립적이고 공개된 순수 함수로 분리되어 있습니다
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
입력은 flat element array 하나이고, 출력은 issue 집합입니다. PDFium function 을 호출하지도, 문서를 열지도, global state 를 건드리지도 않습니다. 이 분리는 의도적이며 두 번 이득을 줍니다. 첫째는 testability 입니다. unit test 안에서 합성 TPdfStructureElements 배열을 만들 수 있습니다. 예를 들어 Alt 가 없는 Figure, 접근 가능한 텍스트가 ActualText 에만 있는 Formula, 같은 ID 를 공유하는 두 Note 를 만든 뒤, pdfium.dll 이 전혀 없어도 결과 집합을 검증할 수 있습니다. 규칙 로직은 오프라인에서 검증되고, DLL traversal 은 라이브 문서 smoke test 로 별도 검증되며, 라이브러리가 없으면 건너뜁니다
둘째는 책임 경계가 분명해진다는 점입니다. TPdf.ValidatePdfUa 는 각 page 를 로드하고 element 를 뽑아 누적하는 지저분한 부분을 맡고, 깨끗한 배열을 순수 checker 에 넘깁니다. "데이터 가져오기"는 DLL, side effect, lifetime 의 문제이고, "규칙 판정"은 순수하고 결정적입니다. 둘이 얽히지 않습니다. 규칙을 바꿔야 할 때 I/O 가 전혀 없는 함수 하나만 고치면 됩니다
세 가지 규칙이 실제로 검사하는 것
structure-tree 패스는 TPdfUaValidationIssues 끝에 ABI 안정성을 유지한 채 추가된 세 issue 값을 올립니다. pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, pvuaiNoteMissingId 입니다. 본문은 완전히 추적할 수 있을 만큼 작습니다
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 를 다룹니다. Figure element 는 text alternative 를 제공해야 합니다. 초기 버전은 Alt entry 만 봤기 때문에 reference validator 보다 더 엄격했습니다. PDF/UA 는 figure 의 접근 가능한 텍스트가 ActualText 로 제공돼도 이를 유효한 대체 표현으로 인정합니다. 그래서 규칙은 Alt 와 ActualText 가 둘 다 비어 있을 때만 Figure 를 오류로 판단합니다. Clause 7.7 은 formula 를 다루며, 같은 보정 이후 Figure 와 동일한 Alt-or-ActualText 검사를 사용합니다. conformance corpus 에서 Formula 의 accessible text 를 ActualText 하나로만 제공한 샘플이 있었는데, 이 분기가 Figure 쪽과 맞춰지기 전에는 거짓으로 반려되고 있었습니다
Clause 7.9 는 성격이 다릅니다. Note 는 /ID 를 가져야 하고, 그 ID 는 문서 전체에서 유일해야 합니다. ID 누락은 per-element 실패입니다. 반면 중복 ID 는 두 element 사이의 관계이므로, flat array 가 중요해집니다. checker 는 각 Note 를 볼 때 이미 본 element 를 뒤로 훑어 같은 ID 를 가진 이전 Note 와 충돌하는지 확인합니다. 비용은 Note 수에 대한 전형적인 O(n²) 이지만, 실제 문서 규모에서는 무시할 수 있고 보조 인덱스를 유지하지 않아도 되므로 함수는 단일하고 읽기 쉬운 루프로 남습니다
페이지를 넘나들며 누적해야 유일성이 전역이 된다
PDFium 은 structure element 를 문서 단위가 아니라 page 단위로 노출하므로, ValidatePdfUa 는 규칙 실행 전에 이를 모아야 합니다. 이 메서드는 각 page 를 FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage 로 순회하고, component 에 현재 어떤 page 가 열려 있든 상관없이 각 page 의 element 를 하나의 배열에 덧붙입니다. 그 다음에야 순수 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 검사가 올바릅니다. 서로 다른 page 에 있는 두 Note 가 같은 ID 를 가질 수 있으며, page 별로 검증하면 각 page 의 element 집합이 내부적으로만 보면 일관돼 보이기 때문에 그 충돌을 영영 보지 못합니다. 문서 전체 배열 하나를 만드는 것만이 중복을 드러내는 방법입니다. 앞쪽 가드도 중요합니다. tree walk 는 byte-level 패스가 not 보고한 경우에만 pvuaiMissingStructTreeRoot 를 기준으로 실행됩니다. untagged 문서는 걸을 tree 가 없고 이미 missing structure root 로 플래그가 올라가 있으므로 page load 는 전부 건너뜁니다. 이 deep pass 는 혜택을 받을 수 없는 문서에서는 비용이 들지 않습니다
보수적으로 설계되었다. 조용히 놓칠지언정, 거짓 경보는 울리지 않는다
이 validator 의 가장 중요한 속성은 일부러 하지 않는 일에 있습니다. 이것은 표준 /S 타입 이름만 매칭하며, FPDF_StructElement_GetType 이 직접 반환하는 Figure, Formula, Note 만 봅니다. custom type 을 정의하고 이를 Figure 로 role-map 한 문서는 PDFium 이 타입을 어떻게 해석하느냐에 따라 자기 이름을 그대로 돌려줄 수 있습니다. 그런 경우 checker 는 그것을 인식하지 못하고 조용히 넘어갑니다. 이는 false negative 이며 의도된 동작입니다. 설계 규칙은 false positive 를 절대로 내지 않으면서 under-report 하는 것 입니다. conformant file 에도 경보를 울리는 preflight 도구는 사용자가 곧 무시하게 되고, 무시되는 validator 는 없는 것보다 더 나쁩니다. decorative image 는 structure tree 가 아니라 artifact stream 에 있으므로 애초에 Figure 로 surface 되지 않으며, 올바르게 artifact 처리된 배경 요소에 대해 "missing Alt" 경고를 받지는 않습니다
세 규칙으로 범위를 좁힌 이유도 같습니다. heading level nesting(7.4), table header scope(7.5), role-map cycle detection(7.1)은 모두 정당한 PDF/UA 요구사항이지만, 이를 잘 검사하려면 실제 graph 및 attribute 분석이 필요하고, 순진하게 검사하면 바로 이 설계가 금지하는 false positive 가 생깁니다. 예를 들어 PDF/UA 는 H1, H2, H3, H3 같은 heading 패턴을 허용하는데, 단순히 "계속 증가해야 한다"는 규칙은 이를 잘못 반려합니다. 그런 검사는 전용 conformance 도구에 맡겨 둡니다. Tier-1 세트는 속성 누락이 애매하지 않은 부분만 담습니다
경계를 분명히 말해 두기
이를 release gate 에 연결하기 전에 알아둘 만한 한계가 두 가지 있습니다. 첫째, checker 는 PDFium 이 structure element 에서 읽어 낼 수 있는 정보만큼만 정확합니다. reference validator 는 통과시키는 conformance-corpus 파일 중 일부는 PDFium 이 surface 하지 않는 alternate-text 메커니즘을 사용하기 때문에, FPDF_StructElement_GetAltText 가 빈 값을 돌려줍니다. 그러면 순수 checker 는 불완전한 데이터 위에서 "정확하게" missing Alt 를 보고하게 됩니다. false positive 의 원인이 규칙 로직이 아니라 DLL accessor coverage 에 있다는 뜻입니다. 이런 경우를 흡수하려고 규칙을 느슨하게 만들면, 본래 잡아야 할 실제 결함도 함께 놓치게 되므로, 이들은 덮어쓰지 않고 알려진 PDFium limitation 으로 문서화됩니다
둘째, 이것은 certification 이 아니라 preflight 입니다. Tier-1 은 byte scan 이 구조적으로 볼 수 없는 high-confidence 콘텐츠 오류를 거짓 경보 없이 잡아내지만, heading semantics, table structure, reading order correctness 를 포함한 전체 PDF/UA 적합성은 여전히 완전한 validator 와 최종적으로는 사람 검토의 영역입니다. ValidatePdfUa 는 자체 파이프라인에서 명백한 결함을 빠르고 저렴하게 실패시키는 데 사용하고, 최종 판단은 veraPDF 또는 PAC 에 맡기면 됩니다. 동일한 structure-tree traversal 은 Delphi용 접근 가능한 PDF reader 구축 에도 쓰이며, 여기서는 tag tree 가 reading order 와 spoken text 를 구동합니다. 또한 Delphi에서 PDF annotation 검토 와 같은 metadata 수준 작업을 보완합니다
여기서 보인 structure-tree API 와 ValidatePdfUa validator 는 Delphi 와 C++Builder(VCL), Lazarus/FPC(LCL) 용 PDFium Component 에 포함되어 있습니다. 제품 페이지에는 전체 TPdfStructureElement record 레이아웃과 이 검사 뒤의 issue enumeration 을 포함한 전체 API reference 링크가 정리되어 있습니다