Articol tehnic

Validarea PDF-urilor Comprimate: Fluxuri de Obiecte și XRef

Scrieți un mic validator. Acesta deschide un PDF, caută până la sfârșit, găsește startxref, citește offset-ul și se așteaptă să aterizeze pe cuvântul cheie xref cu un tabel de referințe încrucișate de lățime fixă sub el. Din acel tabel colectează offset-urile 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 a-l testa. Apoi, un fișier produs de o versiune curentă de Word sau de o bibliotecă care vizează PDF 1.5 ajunge, iar validatorul îl declară defect. Nu există niciun cuvânt cheie xref unde indică offset-ul, 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ă în raport cu aspectul clasic eșuează pe documentele moderne. Structura de care depinde, tabelul de referințe încrucișate în text simplu și cuvântul cheie trailer, au devenit opționale în PDF 1.5 și sunt frecvent absente. Două caracteristici le-au înlocuit: fluxul de referințe încrucișate și fluxul de obiecte comprimate. 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 în legătură cu coada fișierului

ISO 32000-1 §7.5.8 definește fluxul de referințe încrucișate, iar §7.5.7 definește fluxul de obiecte de tip /ObjStm. Împreună, ele permit unui scriitor să renunțe la cele două structuri pe care se bazează un parser clasic. Un fișier PDF 1.5 s-ar putea termina fără niciun tabel xref deloc. Î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 deține datele de referință încrucișată într-o formă binară compactă. Nu există niciun cuvânt cheie trailer, de asemenea, deoarece trailerul este acum propriul dicționar al fluxului. Cheile pe care un parser clasic le-a vânat, /Root, /Size și /ID, trăiesc în interiorul acelui dicționar

A doua schimbare mută obiectele însele. În loc să scrie fiecare obiect indirect la propriul offset de octeți, un scriitor poate împacheta multe obiecte mici, dicționarele de pagini, dicționarele de adnotări, arborele structurii, într-un singur flux de obiecte și poate comprima întregul container cu Flate. Obiectele individuale nu mai au un offset de octeți în fișier. Au o poziție în interiorul unui blob comprimat. Un validator care scanează octeții bruți după 1 0 obj nu îi găsește niciodată, deoarece acel text există doar după umflare (inflation). Pentru un parser clasic, jumătate din document pur și simplu a dispărut

Cheile trailerului sunt text simplu, chiar și într-un fișier comprimat

Partea liniștitoare este că citirea trailerului unui flux de referințe încrucișate nu necesită umflarea a nimic. Un obiect flux este scris ca un dicționar urmat de cuvântul cheie stream și apoi octeții comprimați. Dicționarul este text simplu. Deci, atunci când startxref indică un flux de referințe încrucișate, octeții imediat după numărul obiectului arată ca un dicționar obișnuit, iar /Root, /Size și /ID stau acolo în clar, înainte de a începe cuvântul cheie stream și datele Flate

Asta înseamnă că un validator poate afla cele trei fapte de care are cea mai mare nevoie, unde se află catalogul, câte obiecte revendică fișierul și identificatorul fișierului, analizând doar dicționarul fluxului. Nu trebuie să decomprime datele de referință încrucișată și nu trebuie să interpreteze intrările binare din interiorul acestuia. Munca care învinge un parser naiv nu este citirea trailerului; este găsirea obiectelor. Acestea sunt două probleme separabile, iar rezolvarea primei este ieftină

Fluxuri de obiecte: un antet, apoi un blob Flate

Un flux de obiecte este un container. Dicționarul său poartă /Type /ObjStm, o intrare /N dând numărul de obiecte împachetate înăuntru și o intrare /First dând offset-ul de octeți, în cadrul datelor umflate, unde începe corpul primului obiect. Sarcina utilă comprimată, odată umflată, începe cu un mic antet de /N perechi de numere întregi. Fiecare pereche este un număr de obiect și offset-ul corpului acelui obiect relativ la /First. După antet vin corpurile obiectelor însele, concatenate

Extinderea unuia este mecanică odată ce octeții sunt umflați. Citiți dicționarul pentru a obține /N și /First, umflați fluxul cu un decodor Flate, parcurgeți primele /N perechi pentru a afla ce număr de obiect trăiește la ce offset și apoi extrageți fiecare corp ca și cum ar fi un obiect indirect obișnuit. Singura dependență reală este decodorul Flate, și aveți deja unul: Delphi livrează System.ZLib, iar Free Pascal livrează unitatea zstream, ambele înfășurând zlib și umflâ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 paginilor, să se comporte exact așa cum s-ar comporta pe un fișier clasic

Ce nu trebuie să implementați

Este ușor să supraestimăm munca. Citirea cheilor trailerului dintr-un fișier comprimat nu necesită decodificarea intrărilor binare ale fluxului de referințe încrucișate. Fluxul de referințe încrucișate §7.5.8 folosește trei tipuri de intrare, iar intrarea de tip 2, cea care spune acest obiect trăiește în interiorul fluxului de obiecte N la indexul i, este ceea ce ați decoda pentru a construi o hartă completă de offset-uri. Aveți nevoie de acea hartă pentru a rezolva obiecte arbitrare după număr. Nu aveți nevoie de ea pentru a citi /Root, /Size și /ID, care sunt în dicționarul 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 predictoare PNG și TIFF pe care un flux de referințe încrucișate le-ar putea aplica prin /DecodeParms al său doar pentru a obține cheile trailerului. Predictorii filtrează rândurile binare de referințe încrucișate pentru a le face să se comprime mai bine; nu au nimic de-a face cu dicționarul care precede fluxul. Actualizarea minimă care face un validator clasic conștient de PDF-urile moderne este, prin urmare, mică: atunci când startxref aterizează pe un flux în loc de cuvântul cheie xref, analizați dicționarul fluxului pentru cheile trailerului ș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 rezolvarea aleatoare a obiectelor

De ce o verificare de conformitate trebuie să extindă mai întâi fluxurile

Acest lucru încetează să mai fie academic î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 potrivit, fiecare descriptor de font pentru un fișier de font încorporat, trailerul pentru un /ID. Într-un fișier comprimat, majoritatea acelor 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 lipsindu-i intenția de ieșire (output intent), lipsindu-i XMP-ul și lipsindu-i jumătate din structura sa, deoarece dovezile de care are nevoie încă se află într-un blob Flate pe care nu l-a umflat niciodată

Ordinea contează. Extinderea trebuie să aibă loc înainte de a rula verificările, nu odată 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, ea moștenește orbirea parserului clasic și produce încălcări false exact pe fișierele moderne care sunt cele mai susceptibile de a fi bine formate, deoarece au ieșit din lanțuri de instrumente suficient de noi pentru a scrie fluxuri de referințe încrucișate în primul rând

Lăsând PDFium să facă parsarea pentru dvs

Componenta PDFium analizează fluxurile de referințe încrucișate și fluxurile de obiecte ca parte a încărcării unui document, care este modalitatea practică de a evita rularea manuală a pasului de umflare și extindere (inflate-and-expand). Când încărcați un fișier cu componenta TPdf, obiectele împachetate în containerele /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 precum 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ă doar atunci când a fost detectat un nivel de conformitate și setul de probleme este gol. Deoarece obiectele au fost extinse în timpul încărcării, un tablou /OutputIntents sau un font încorporat care a trăit în interiorul unui flux de obiecte este găsit, nu raportat 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ă și la ValidatePdfX, care returnează un TPdfXValidationResult cu aceeași formă. Ideea rutării prin PDFium este că decompresia structurală descrisă mai sus are loc o singură dată, corect, în interiorul încărcătorului, astfel încât codul dvs. 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

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
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: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues is a set: count its members
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Dacă octeții sunt deja în memorie mai degrabă decât pe disc, aceeași secvență de încărcare-apoi-validare funcționează prin supraîncărcarea LoadDocument(const Data: TBytes), care preia conținutul brut al fișierului și analizează fluxurile sale de referințe încrucișate și de obiecte în același mod în care o face calea fișierului. Ceea ce trebuie reținut pentru un validator scris de mână este regula structurală, nu API-ul: citiți cheile trailerului din dicționarul fluxului în text simplu, extindeți fiecare /ObjStm cu un decodor Flate înainte de a parcurge documentul și tratați decodificarea intrărilor binare de referințe încrucișate ca pe sarcina mai mare, opțională care este

Odată ce structura este extinsă, un validator poate conduce restul unui flux de lucru peste ea. Pentru un ham preflight (preflight harness) în linia de comandă care raportează conformitatea pe un folder de intrări, consultați ghidul nostru pas cu pas despre construirea unui CLI de raport preflight în lot (batch). Atunci când validarea este o poartă înaintea descompunerii unui document mare, tehnicile din ghidul nostru pentru împărțirea 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 Componentei PDFium pentru Delphi și C++Builder