Preflight Anda menyatakan file ini bersih dari PDF/UA. veraPDF membuka file yang sama dan menandai sebuah Figure tanpa teks alternatif di klausul 7.3. Kedua alat itu benar, dan celah di antara keduanya adalah inti masalah saat aksesibilitas dicek hanya dengan pemindaian byte. Pemeriksaan di level byte memang memastikan file mengaku bertag: ia menemukan /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part di paket XMP, judul dokumen, dan bahasa. Itu semua penanda format, perlu tetapi tidak cukup. Penanda itu tidak memberi tahu apakah figure di halaman empat benar-benar punya deskripsi yang bisa dibacakan pembaca layar. Jawaban itu ada di tag tree, dan untuk mendapatkannya Anda harus menelusuri tree
PDFium Component adalah pustaka PDF VCL native untuk Delphi dan C++Builder, dan ValidatePdfUa menjalankan dua tahap itu. Tahap byte-level menangani penanda format. Di atasnya ada tahap structure-tree yang memuat tag tree hidup, menelusuri setiap elemen, dan memeriksa seperangkat kecil aturan konten berkeyakinan tinggi, di mana atribut yang hilang berarti cacat aksesibilitas nyata, bukan preferensi gaya. Artikel ini membahas tahap kedua itu: apa yang diperiksa, mengapa logika aturannya berupa pure function tanpa DLL di bawahnya, dan di mana ia sengaja berhenti
Mengapa pemindaian byte tidak bisa melihat Alt yang hilang
ISO 14289-1 (PDF/UA-1) adalah lapisan persyaratan di atas ISO 32000. Sebagian persyaratan bersifat struktural dan terlihat di file mentah: katalog harus mendeklarasikan structure tree, viewer preferences harus menetapkan DisplayDocTitle, font harus disematkan. Pemindai token yang membuang isi stream dan mencocokkan token nama berdasarkan batas delimiter bisa memverifikasi semuanya itu, dan ValidatePdfUaCompliance milik PDFium memang melakukan hal itu untuk klausul seperti 7.1, 7.18, dan 7.21
Tetapi "setiap Figure memiliki alternate text" bukan sifat sintaks file. Itu adalah sifat struktur logis, yaitu tree elemen bertag yang memetakan konten ke makna. Entri Alt pada Figure bisa berada di dictionary elemen struktur, disuplai lewat span /ActualText, atau datang dari jenis kustom yang dipetakan melalui role map. Anda tidak bisa menemukannya dengan andal hanya dengan mencari /Alt di stream byte, karena string itu muncul dalam konteks lain, bisa terkompresi di object stream, dan tidak memberi tahu elemen struktur mana yang memilikinya. Cara yang jujur untuk menjawab pertanyaan itu adalah bertanya ke structure tree dokumen itu sendiri, elemen demi elemen, seperti yang dilakukan veraPDF dan PAC. Itulah garis yang dipakai pemeriksaan Tier-1 PDFium: byte scan untuk format, tree walk untuk konten
Membaca tag tree hidup
Bahan mentahnya adalah TPdf.GetStructureElements (juga diekspos sebagai properti StructureElements), yang mengembalikan TPdfStructureElements - array datar dari record TPdfStructureElement dalam urutan dokumen. Setiap record adalah proyeksi satu elemen struktur melalui fungsi akses PDFium, dengan field yang benar-benar dibutuhkan aturan aksesibilitas:
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 TypeName adalah titik tumpu validator. Nilainya berasal dari FPDF_StructElement_GetType, yang mengembalikan tipe struktur standar elemen itu, yaitu nama /S-nya, setelah PDFium menyelesaikan role map. AlternateText berasal dari FPDF_StructElement_GetAltText, ActualText dari FPDF_StructElement_GetActualText, dan ID dari FPDF_StructElement_GetID. Karena array-nya datar dan terurut, validator bisa menalar seluruh dokumen sekaligus tanpa rekursi, dan itu penting untuk satu aturan yang bersifat global, bukan per-elemen
Checker ini pure function, dan itu disengaja
Logika aturan tidak hidup di dalam method yang berbicara dengan DLL. Ia adalah pure function publik yang berdiri sendiri:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Ia menerima array elemen datar dan mengembalikan sekumpulan issue. Ia tidak memanggil fungsi PDFium apa pun, tidak membuka dokumen, dan tidak menyentuh state global. Pemisahan ini disengaja, dan manfaatnya dua kali lipat. Pertama, testability: Anda bisa membuat array TPdfStructureElements sintetis di unit test, sebuah Figure tanpa Alt, sebuah Formula yang satu-satunya teks aksesibelnya ada di ActualText, dua Note yang berbagi ID, lalu memeriksa hasilnya tanpa pdfium.dll sama sekali. Logika aturan diverifikasi secara offline, sedangkan traversal DLL diverifikasi terpisah oleh smoke test dokumen hidup yang dilewati saat library tidak ada
Kedua, kejelasan tanggung jawab. TPdf.ValidatePdfUa memegang bagian yang berantakan, memuat tiap halaman, mengambil elemennya, mengakumulasikannya, lalu menyerahkan array yang bersih ke checker murni. "Ambil datanya" (DLL, efek samping, lifetime) dan "nilai aturannya" (murni, deterministik) tidak pernah tercampur. Saat aturan perlu diubah, Anda mengubah fungsi yang tidak punya I/O di dalamnya
Apa yang sebenarnya dicek oleh tiga aturan
Tahap structure-tree menghasilkan tiga nilai issue, ditambahkan di akhir TPdfUaValidationIssues agar enum tetap ABI-stable untuk pemanggil yang sudah ada: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, dan pvuaiNoteMissingId. Isi fungsinya cukup kecil untuk dipahami sepenuhnya:
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 mengatur figure: elemen Figure harus menyediakan alternatif teks. Versi awal pemeriksaan ini hanya melihat entri Alt, sehingga lebih ketat daripada validator rujukan. PDF/UA menerima figure yang teks aksesibelnya disuplai melalui ActualText juga, jadi aturan ini menandai Figure hanya ketika Alt dan ActualText sama-sama kosong. Klausul 7.7 mencakup formula, dan setelah koreksi yang sama ia memakai tes Alt-atau-ActualText yang identik; satu sampel corpus kepatuhan yang memberi teks aksesibel Formula hanya lewat ActualText sebelumnya ditolak keliru sampai cabang Formula diselaraskan dengan cabang Figure
Klausul 7.9 berbeda secara jenis. Note harus memiliki /ID, dan ID itu harus unik di seluruh dokumen. ID yang hilang adalah kegagalan per elemen. ID yang duplikat adalah hubungan antara dua elemen, itulah sebabnya array datar itu penting: untuk setiap Note, checker menelusuri mundur elemen yang sudah dilihat dan menandai benturan dengan Note sebelumnya yang memakai ID sama. Biayanya jelas O(n²) terhadap jumlah Note, yang tidak relevan untuk dokumen nyata mana pun dan membuat fungsi tetap berupa satu loop yang mudah dibaca tanpa indeks tambahan yang harus disinkronkan
Mengakumulasi lintas halaman supaya keunikan bersifat global
PDFium mengekspos structure elements per halaman, bukan per dokumen, jadi orkestrasi di ValidatePdfUa harus mengumpulkannya sebelum aturan dijalankan. Ia menelusuri setiap halaman dengan FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, terlepas dari halaman mana yang sedang dibuka component, dan menambahkan semua elemen tiap halaman ke satu array. Baru setelah itu ia memanggil 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;
Akumulasi inilah yang membuat pemeriksaan keunikan 7.9 benar. Dua Note di halaman berbeda bisa berbagi ID; jika Anda memvalidasi per halaman, benturannya tidak akan pernah terlihat, karena set elemen tiap halaman tampak konsisten di dalam dirinya sendiri. Membangun satu array untuk seluruh dokumen adalah satu-satunya cara agar duplikasi itu tampak. Guard di depan juga penting: tree walk hanya berjalan ketika tahap byte-level tidak melaporkan pvuaiMissingStructTreeRoot. Dokumen tanpa tag tidak punya tree untuk ditelusuri dan sudah ditandai karena struktur root hilang, jadi pemuatan per halaman dilewati seluruhnya. Tahap mendalam ini tidak menambah biaya pada dokumen yang memang tidak bisa memanfaatkannya
Ketat secara desain, diam ketika tidak yakin
Sifat paling penting validator ini adalah apa yang menolak ia lakukan. Ia hanya mencocokkan nama tipe /S standar yang dikembalikan langsung oleh FPDF_StructElement_GetType, Figure, Formula, Note. Dokumen yang mendefinisikan tipe kustom dan memetakan perannya ke Figure akan, tergantung cara PDFium menyelesaikan tipe itu, melaporkan namanya sendiri. Jika itu terjadi, checker tidak mengenalinya dan tetap diam. Itu false negative, dan itu memang perilaku yang diinginkan. Aturan desainnya adalah lebih baik under-report daripada pernah menghasilkan false positive, karena alat preflight yang sering menuduh file patuh sebagai salah akan membuat penggunanya mengabaikannya, dan validator yang diabaikan lebih buruk daripada tidak ada sama sekali. Gambar dekoratif berada di stream artifact, bukan di structure tree, jadi sejak awal mereka tidak pernah muncul sebagai Figure; Anda tidak akan mendapat keluhan "missing Alt" untuk aturan latar yang memang ditandai sebagai artifact
Ini juga alasan cakupannya hanya tiga aturan. Penanaman heading level (klausul 7.4), cakupan header tabel (7.5), dan deteksi siklus role map (7.1) semuanya adalah persyaratan PDF/UA yang sah, tetapi memeriksanya dengan baik membutuhkan analisis graf dan atribut yang nyata, dan memeriksanya secara naif justru menghasilkan false positive yang dilarang desain, PDF/UA mengizinkan pola heading seperti H1, H2, H3, H3 yang oleh aturan sederhana "harus selalu meningkat" akan salah ditolak. Pemeriksaan itu dibiarkan kepada alat kepatuhan khusus. Set Tier-1 adalah subset di mana atribut yang hilang memang tidak ambigu
Batasnya, dinyatakan terang
Dua batas layak diketahui sebelum Anda memasang ini ke release gate. Pertama, checker ini hanya sebaik apa yang bisa dibaca PDFium dari structure element. Sejumlah kecil file dalam conformance corpus yang lolos di validator rujukan memakai mekanisme alternate text yang tidak diekspos PDFium, sehingga FPDF_StructElement_GetAltText mengembalikan kosong walaupun file itu benar-benar patuh. Pure checker lalu secara "benar" menandai Alt yang hilang pada data yang tidak lengkap, sebuah false positive yang berasal dari cakupan accessor DLL, bukan dari logika aturan. Melonggarkan aturan untuk menampung kasus-kasus itu juga akan membutakannya terhadap kegagalan nyata yang memang ingin ia tangkap, jadi kasus itu didokumentasikan sebagai keterbatasan PDFium yang sudah dikenal, bukan ditutup-tutupi
Kedua, ini preflight, bukan sertifikasi. Tier-1 menangkap error konten berkeyakinan tinggi yang secara struktural tidak bisa dideteksi oleh scan byte, dan ia melakukannya tanpa alarm palsu, tetapi kepatuhan PDF/UA penuh, termasuk semantik heading, struktur tabel, dan kebenaran urutan baca, tetap milik validator lengkap dan pada akhirnya seorang reviewer manusia. Gunakan ValidatePdfUa untuk menggagalkan defect yang jelas secara cepat dan murah di pipeline Anda sendiri, lalu biarkan veraPDF atau PAC memberi keputusan final. Traversal structure tree yang sama juga menjadi dasar saat membangun pembaca PDF aksesibel di Delphi, di mana tag tree menggerakkan urutan baca dan teks yang dibacakan, dan ia melengkapi pekerjaan di level metadata saat meninjau anotasi PDF dari Delphi
API structure-tree dan validator ValidatePdfUa yang ditunjukkan di sini tersedia bersama PDFium Component untuk Delphi dan C++Builder (VCL) serta Lazarus/FPC (LCL). Halaman produk menautkan referensi API lengkap, termasuk layout record TPdfStructureElement yang utuh dan enumerasi issue di balik pemeriksaan ini