Scrieți un mic validator. Deschide un PDF, caută la sfârșit, găsește startxref, citește decalajul (offset) și se așteaptă să aterizeze pe cuvântul cheie xref cu un tabel de referință încrucișată de lățime fixă dedesubt. Din acel tabel colectează decalajele obiectelor, apoi scanează înapoi după cuvântul cheie trailer pentru a afla /Root și /Size. Funcționează perfect pe fiecare fișier pe care l-ați generat pentru testare. Apoi sosește un fișier produs de o versiune curentă de Word, sau de o bibliotecă ce vizează PDF 1.5, iar validatorul îl declară corupt. Nu există niciun cuvânt cheie xref unde indică decalajul, niciun dicționar trailer nicăieri, iar tabelul de obiecte pe care l-a construit validatorul este aproape gol. Fișierul este valid. Validatorul îl citește printr-o lentilă veche de cincisprezece ani.
Acesta este cel mai frecvent motiv pentru care o verificare PDF la nivel de octet scrisă pentru structura clasică eșuează pe documentele moderne. Structura de care depinde, tabelul de referință încrucișată în text simplu și cuvântul cheie trailer, a fost făcută opțională în PDF 1.5 și este frecvent absentă. Două caracteristici au înlocuit-o: fluxul de referință încrucișată (cross-reference stream) și fluxul de obiecte comprimat (compressed object stream). Ambele sunt descrise în ISO 32000-1, iar un validator care nu știe despre ele vede un fișier sănătos ca pe o grămadă de obiecte lipsă.
Ce a schimbat PDF 1.5 la sfârșitul fișierului
ISO 32000-1 §7.5.8 definește fluxul de referință încrucișată, iar §7.5.7 definește fluxul de obiecte de tip /ObjStm. Împreună, acestea permit unui scriitor să renunțe la cele două structuri pe care un parser clasic le caută. Un fișier PDF 1.5 se poate termina fără niciun tabel xref. În locul său, obiectul către care indică startxref este un obiect flux obișnuit al cărui dicționar poartă /Type /XRef, iar acel flux conține datele de referință încrucișată într-o formă binară compactă. De asemenea, nu mai există cuvântul cheie trailer, deoarece trailer-ul este acum propriul dicționar al fluxului. Cheile pe care le căuta un parser clasic, /Root, /Size și /ID, trăiesc în interiorul acelui dicționar.
A doua modificare mută obiectele în sine. În loc să scrie fiecare obiect indirect la propriul decalaj de octeți, un scriitor poate împacheta mai multe obiecte mici (dicționarele de pagină, dicționarele de adnotări, arborele de structură) într-un singur flux de obiecte și poate comprima întregul container cu Flate. Obiectele individuale nu mai au un decalaj de octeți în fișier. Ele au o poziție în interiorul unui blob comprimat. Un validator care scanează octeții bruți după 1 0 obj nu le găsește niciodată, deoarece acel text există doar după decompresie (inflation). Pentru un parser clasic, jumătate din document a dispărut pur și simplu.
Cheile trailer sunt în text simplu, chiar și într-un fișier comprimat
Partea liniștitoare este că citirea trailer-ului unui flux de referință încrucișată nu necesită decomprimarea datelor. Un obiect flux este scris ca un dicționar urmat de cuvântul cheie stream și apoi de octeții comprimați. Dicționarul este în text simplu. Astfel, când startxref indică spre un flux de referință încrucișată, octeții imediat de după numărul obiectului arată ca un dicționar obișnuit, iar /Root, /Size și /ID se află acolo în clar, înainte de începerea cuvântului cheie stream și a datelor Flate.
Asta înseamnă că un validator poate afla cele trei date de care are cea mai mare nevoie (unde este catalogul, câte obiecte declară fișierul și identificatorul fișierului) parsand doar dicționarul fluxului. Nu trebuie să decompileze datele de referință încrucișată și nu trebuie să intrepreteze intrările binare din interiorul acestora. Munca care învinge un parser naiv nu este citirea trailer-ului, ci găsirea obiectelor. Acestea sunt două probleme distincte, iar rezolvarea primei este simplă.
Fluxuri de obiecte: un antet, apoi un blob Flate
Un flux de obiecte este un container. Dicționarul său conține /Type /ObjStm, o intrare /N care indică numărul de obiecte împachetate în interior și o intrare /First care indică decalajul de octeți, în cadrul datelor decomprimate, unde începe corpul primului obiect. Încărcătura comprimată, odată decomprimată, începe cu un mic antet de /N perechi de întregi. Fiecare pereche este un număr de obiect și decalajul corpului acelui obiect în raport cu /First. După antet urmează corpurile obiectelor în sine, concatenate.
Extinderea unuia este mecanică odată ce octeții sunt decomprimați. Citiți dicționarul pentru a obține /N și /First, decomprimați fluxul cu un decodificator Flate, parcurgeți cele /N perechi de la început pentru a afla ce număr de obiect trăiește la ce decalaj și apoi extrageți fiecare corp ca și cum ar fi un obiect indirect obișnuit. Singura dependență reală este decodificatorul Flate și aveți deja unul: Delphi livrează System.ZLib, iar Free Pascal livrează unitatea zstream, ambele împachetând zlib și decomprimând un flux Flate brut fără niciun cod terț. O rutină care adaugă fiecare obiect extras la tabelul de obiecte al validatorului face ca restul validatorului, partea care parcurge /Root și verifică arborele de pagini, să se comporte exact ca pe un fișier clasic.
Ce nu trebuie să implementați
Este ușor să supraestimați munca. Citirea cheilor trailer dintr-un fișier comprimat nu necesită decodificarea intrărilor binare ale fluxului de referință încrucișată. Fluxul de referință încrucișată §7.5.8 folosește trei tipuri de intrări, iar intrarea de tip 2, cea care spune acest obiect trăiește în fluxul de obiecte N la indexul i
, este cea pe care ar trebui să o decodificați pentru a construi o hartă completă a decalajelor. Aveți nevoie de acea hartă pentru a rezolva obiecte arbitrare după număr. Nu aveți nevoie de ea însă pentru a citi /Root, /Size și /ID, care se află în dicționarul în text simplu, și nu aveți nevoie de ea pentru a extinde fluxurile de obiecte, deoarece fiecare /ObjStm își anunță propriul conținut prin /N și /First.
De asemenea, nu trebuie să gestionați funcțiile predictori PNG și TIFF pe care un flux de referință încrucișată le poate aplica prin /DecodeParms doar pentru a obține cheile trailer. Predictorii filtrează rândurile binare de referință încrucișată pentru a le comprima mai bine; nu au nicio legătură cu dicționarul care precede fluxul. Actualizarea minimă care face un validator clasic compatibil cu PDF-urile moderne este mică: când startxref aterizează pe un flux în loc de cuvântul cheie xref, parsați dicționarul fluxului pentru cheile trailer și extindeți orice obiecte /ObjStm pe care le întâlniți, astfel încât conținutul lor să intre în tabelul de obiecte. Decodificarea intrărilor de tip 2 și a predictorilor este o sarcină separată, mai mare, pe care o puteți amâna până când aveți cu adevărat nevoie de rezoluție aleatorie a obiectelor.
De ce o verificare de conformitate trebuie să extindă fluxurile mai întâi
Acest lucru încetează să mai fie teoretic în momentul în care rulați o verificare de profil. Un validator PDF/A sau PDF/X inspectează obiecte specifice: catalogul documentului pentru un tablou /OutputIntents, fluxul /Metadata pentru un pachet XMP cu identificatorul corect, fiecare descriptor de font pentru un fișier de font încorporat, trailer-ul pentru un /ID. Într-un fișier comprimat, majoritatea acestor obiecte se află în interiorul fluxurilor de obiecte. Un validator care nu a extins fluxurile de obiecte nu poate vedea cheile catalogului, nu poate găsi metadatele și nu poate enumera fonturile. Va raporta un document perfect conform ca fiind lipsit de intenție de ieșire, lipsit de XMP și lipsit de jumătate din structură, deoarece dovezile de care are nevoie se află încă într-un blob Flate pe care nu l-a decomprimat niciodată.
Ordinea contează. Extinderea trebuie să aibă loc înainte ca verificările să ruleze, nu în paralel cu ele, deoarece fiecare verificare presupune că poate ajunge la un obiect după număr. Dacă conectați o verificare de profil direct la o scanare brută de octeți, moșteniți orbirea parserului clasic și produceți erori false pe exact fișierele moderne care sunt cel mai probabil bine formate, deoarece provin din instrumente suficient de noi pentru a scrie fluxuri de referință încrucișată în primul rând.
Lăsați PDFium să facă parsarea pentru dumneavoastră
Componenta PDFium parcurge fluxurile de referință încrucișată și fluxurile de obiecte ca parte a încărcării unui document, ceea ce reprezintă modalitatea practică de a evita scrierea manuală a pasului de decompresie și extindere. Când încărcați un fișier cu componenta TPdf, obiectele împachetate în containere /ObjStm sunt deja rezolvate, iar punctele de intrare de validare văd documentul complet extins. ValidatePdfA returnează o înregistrare TPdfAValidationResult al cărei câmp Conformance este o valoare TPdfAConformance cum ar fi pac1b sau pacNone, al cărei câmp Issues este un set de probleme specifice găsite și a cărei metodă IsCompliant este adevărată numai când s-a detectat un nivel de conformitate iar setul de probleme este gol. Deoarece obiectele au fost extinse în timpul încărcării, un tablou /OutputIntents sau un font încorporat care trăia în interiorul unui flux de obiecte este găsit, nu raportat ca fiind lipsă.
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
Același lucru se aplică la ValidatePdfX, care returnează un TPdfXValidationResult cu aceeași structură. Scopul trecerii prin PDFium este ca decompresia structurală descrisă mai sus să aibă loc o singură dată, corect, în interiorul loader-ului, astfel încât codul dumneavoastră de validare să nu vadă niciodată diferența dintre un fișier clasic și unul complet comprimat. Ambele ajung la validator ca un set rezolvat de obiecte.
var
Pdf: TPdf;
R : TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Dacă octeții sunt deja în memorie, mai degrabă decât pe disc, aceeași secvență de încărcare și validare funcționează prin supraîncărcarea LoadDocument(const Data: TBytes), care preia conținutul brut al fișierului și parcurge fluxurile sale de referință încrucișată și de obiecte în același mod ca calea fișierului. Ideea principală pentru un validator scris manual este regula structurală, nu API-ul: citiți cheile trailer din dicționarul fluxului în text simplu, extindeți fiecare obiect /ObjStm cu un decodificator Flate înainte de a parcurge documentul și tratați decodificarea intrărilor binare de referință încrucișată ca fiind munca suplimentară și opțională care este.
Odată ce structura este extinsă, un validator poate derula restul fluxului de lucru peste ea. Pentru un instrument de linie de comandă preflight care raportează conformitatea pe un folder de intrări, consultați ghidul nostru despre construirea unui CLI de raportare preflight în loturi. Când validarea este o etapă înainte de divizarea unui document mare, tehnicile din ghidul nostru de divizare a documentelor PDF în mai multe fișiere se asociază în mod natural cu modelul de încărcare și verificare prezentat aici. Ambele se bazează pe suprafața de încărcare și validare a PDFium Component pentru Delphi și C++Builder.