Un control de ingestie pentru arhivă a respins un lot de fișiere "PDF/A-2b" care se deschideau fără probleme în orice vizualizator de pe birou. Furnizorul jura că erau conforme. Nu erau: fiecare conținea o acțiune JavaScript ascunsă în catalog, genul de lucru pe care un ochi grăbit nu îl observă niciodată și pe care un validator PDF/A complet precum veraPDF îl marchează imediat. Problema este că nimeni nu voia să atașeze un toolchain Java unui serviciu batch Delphi doar ca să răspundă la o singură întrebare da sau nu pentru fiecare fișier. Acesta este golul ValidatePdfACompliancepe care îl umple PDFium Component, și merită înțeles cum ajunge la un verdict fără să parseze complet vreun stream de conținut
De ce PDFium însuși nu poate răspunde la asta
Primul lucru de spus pe față: componenta pdfium.dll PDFium inclusă nu are deloc capabilități PDF/A. Nu există ConvertToPDFA, nici writer de OutputIntent, nici API XMP în suprafața publică. Toată partea de PDF/A din această bibliotecă, atât cea de scriere cât și cea de verificare, trăiește în Pascal pur în FPdfPdfa.pas și funcționează prin parsare la nivel de octet plus actualizare incrementală. Așadar, când chemi validatorul nu îi ceri nimic randorului Chromium. Rulezi un scanner de tokenuri Pascal peste octeții structurali ai fișierului
Interfața publică este deliberat mică. O singură funcție citește un stream de la poziția 0 și returnează un record:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant codifică regula care contează într-un gate: un fișier trece doar când a fost detectat un nivel real de conformitate și setul de probleme este gol. O parsare care reușește dar nu găsește niciun marker pdfaid ajunge la pacNone, ceea ce în mod explicit nu este un pass. Acesta este același punct pe care CLI-ul de raport preflight batch îl face din exterior: un set gol de constatări pentru un fișier nerecunoscut nu înseamnă că totul este în regulă
Eliminarea corpurilor de stream înainte de orice scanare de tokenuri
Iată cel mai important detaliu de implementare, și cel mai ușor de greșit dacă scrii propriul scanner. Detectorul găsește încălcări căutând tokenuri de nume delimitate, lucruri precum /JavaScript, /LZWDecode, /BM. Dacă scanezi octeții brute ai fișierului, corpurile binare de stream încorporate, imaginile comprimate, profilele ICC, programele de fonturi, vor conține la întâmplare secvențe de octeți care arată ca acele tokenuri. Vei raporta /AA sau /3D „găsit” doar pentru că trei octeți dintr-un JPEG au nimerit să scrie asta. Asta e o fabrică de alarme false
Soluția este PdfStructureBytes: parcurge fișierul și înlocuiește cu spații octeții dintre fiecare stream și endstream cuvânt-cheie, lăsând structura dicționarului intactă. Abia după aceea rulează scanarea. Fiecare verificare a tokenurilor de nume din validator operează pe această copie curățată. Dacă iei o singură idee din articolul acesta, ia-o pe aceasta. Aceeași disciplină este oglindită în validatorul PDF/UA, care își păstrează propria copie a rutinei deoarece cele două standarde evoluează independent
Cele 29 de probleme și ce înseamnă fiecare
TPdfAValidationIssue este un contract documentat. Ordinalele sunt fixe pentru că testele DUnitX, demo-urile și stratul de raport depind de ele, așa că noile constatări sunt doar adăugate la final. Începând cu v1.63.0 există 29 de membri. Ei se împart în câteva familii:
- Metadate și identitate:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Culoare și ieșire:
pvaiMissingOutputIntent,pvaiMissingIccProfile, șipvaiMixedDeviceColorSpacescând apar atât DeviceRGB, cât și DeviceCMYK (6.2.3.3) - Interdicții stricte pentru fiecare parte:
pvaiEncryptionPresent(un/Encryptdicționar este interzis complet),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fonturi:
pvaiFontNotEmbeddedși cerința mai strictăpvaiUnembeddedFont, pluspvaiUnicodeMappingMissingpentru o afirmație Level U fără/ToUnicode - Etichetare:
pvaiLevelAStructureMissingcând o afirmație conformance=A nu are structură etichetată
Cei mai noi șase membri, adăugați la pozițiile 24 până la 29, acoperă cazurile subtile în care evaluatorii se împiedică de fapt: pvaiTrappedTrue (un /Trapped /True în dicționarul Info, un "false friend", deoarece valoarea trebuie să fie False sau Unknown), pvaiForbiddenActionSubtype (Sound sau Movie folosit ca acțiune, nu doar ca adnotare), pvaiTransparentColorSpace (un mod de amestecare non-Normal sau un /CA//ca care nu este egal cu 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, și pvaiMixedDeviceColorSpaces
Filtrare în funcție de parte: A-1 este strict, A-2 și A-3 relaxează
PDF/A nu este un singur set de reguli. Trei lucruri interzise de PDF/A-1 sunt permise explicit începând cu PDF/A-2: transparența (un /Transparency grup sau un /SMask, 6.4), conținut opțional (/OCProperties, 6.1.13), și fișierele încorporate (/EmbeddedFiles sau /EF, 6.1.11). Un validator naiv care marchează toate trei pentru fiecare fișier va respinge în masă documente PDF/A-2 perfect valide
Așadar, validatorul citește numărul părții din marcajul pdfaid prin PdfAPartOf și pune acele verificări sub controlul PartNo = 1. Verificările pentru blend-mode și annotation-alpha pentru noile probleme de transparență fac, la fel, parte doar din part-1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Un default conservator merită menționat: atunci când nu există deloc un marcaj pdfaid, partea este tratată ca 1, cea mai strictă. Raționamentul este că un fișier neidentificat ar trebui supus celor mai stricte reguli, nu lăsat să treacă. JavaScript, acțiunile interzise, LZW, XFA, NeedAppearances, adnotările interzise și fonturile neîncorporate rămân interzise pentru fiecare parte, așa că aceste verificări nu stau niciodată în spatele porții
Extinderea object stream-urilor, astfel încât nimic să nu rămână ascuns
PDF 1.5 a introdus cross-reference stream-ul și object stream-ul (/Type /ObjStm), iar acestea creează o zonă oarbă pentru un scanner naiv la nivel de octet. Un catalog, un OutputIntent, un dicționar de acțiune, orice lucru care nu este el însuși un stream, poate fi comprimat cu Flate în interiorul unui ObjStm. Scanezi structura brută și nu vei vedea nimic, apoi vei raporta un fișier curat care este orice altceva decât curat
PdfExpandObjectStreams închide acest gol. Înainte ca orice verificare să ruleze, validatorul face Data := PdfExpandObjectStreams(Data). Rutina găsește fiecare ObjStm, îi citește /N și /First antetul pentru a obține numerele și offseturile obiectelor conținute, decomprimă corpul cu PdfInflate (RTL zlib, System.ZLib pe Delphi și zstream pe FPC), și adaugă fiecare obiect conținut ca un N 0 obj ... endobj la sfârșitul unei copii a octeților. Verificările de token-uri existente găsesc apoi acele obiecte fără nicio schimbare a logicii lor
Două constrângeri fac asta curat, nu fragil. Obiectele de tip stream, Metadata, ICC profile și programele de fonturi nu pot trăi într-un object stream, doar dicționarele non-stream pot, așa că extinderea se ocupă doar de dicționare, iar obiectele adăugate nu poartă niciun stream cuvânt-cheie care să deranjeze pasul de eliminare a corpului. Și pentru că conținutul adăugat ajunge după %%EOF, căutarea inversă de la startxref încă găsește trailerul original. Trailerul stream-ului de cross-reference fusese deja tratat mai devreme, în v1.49.3, prin citirea lui Root, Size și ID direct din dicționarul xref-stream în text clar, un subiect explorat în piesa însoțitoare despre validarea object stream-urilor și cross-reference stream-urilor; munca pe object stream-uri a trebuit doar să adauge pasul de decomprimare, fără nevoie să decodeze intrările xref de tip 2 sau să inverseze un predictor PNG
Limitele oneste ale unui verificator la nivel de octet
Acesta este un instrument de preflight, nu un validator certificat, iar limitele sunt reale. Încorporarea fonturilor este o euristică de numărare, iar să o faci corect a cerut o corecție care merită știută. Verificarea originală folosea PdfCountName('/FontDescriptor'), dar fiecare font contribuie cu două /FontDescriptor token-uri, una o referință din dicționarul fontului și una /Type în obiectul descriptor în sine, așa că numărătoarea era 2N pentru N programe încorporate și testul era mereu adevărat. Corecția este PdfCountDescriptorRefs, care numără doar forma de referință /FontDescriptor N G R și declanșează pvaiUnembeddedFont doar atunci când programele încorporate sunt cu adevărat mai puține:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Chiar și corectată, rămâne grosieră: un document mixt în care fiecare descriptor se întâmplă să aibă un FontFile poate totuși să lase să treacă un font individual neconform. Extinderea object stream-urilor are și un efect secundar cunoscut, scoate la iveală resursele implicite standard-14 pe care le conține un AcroForm /DR cum ar fi /Helv, iar euristica le raportează corect ca neîncorporate, deși veraPDF le permite deoarece nu sunt niciodată folosite efectiv la randare. Verificările la nivel de operator din fluxul de conținut (6.2.10) sunt complet în afara domeniului, deoarece ar necesita parsarea completă a conținutului, nu un scan de octeți. Tratați validatorul ca un prim prag rapid, fără dependențe, care prinde încălcările pe care injectarea marcajului nu le poate repara, și păstrați un validator complet pentru certificarea finală
Aceasta este partea de verificare a poveștii. Partea complementară de scriere, unde SaveAsPdfA injectează XMP, OutputIntent și profilul ICC sRGB și coboară onest o cerere Level A care nu are structură etichetată, se bazează pe aceeași mecanică la nivel de octet. Ambele jumătăți sunt livrate în PDFium Component for Delphi, un singur pachet VCL peste o implementare PDF/A pur Pascal, fără runtime extern de instalat