Deschide un PDF pe care l-au produs Microsoft Word sau Excel, parcurge-l pagină cu pagină, și nimic nu pare neobișnuit. Încarcă-l într-un program Delphi, citește înapoi numărul de pagini, și numărul este corect. Apoi resalvează-l cu criptarea pornită și operațiunea eșuează cu un EListError, sau rezultatul se deschide cu un avertisment de referință încrucișată (cross-reference) deteriorată. Fișierul nu a fost niciodată corupt. Este un fișier cu referință hibridă, și însăși structura care permite unui vizualizator (viewer) vechi de cincisprezece ani să-l deschidă este structura care învinge un încărcător (loader) care se oprește din citit prea devreme
Acesta este unul dintre cele mai comune moduri în care o conductă (pipeline) PDF care a trecut fiecare test intern întâlnește un fișier pe care nu-l poate procesa tur-retur (round-trip). Intrările au fost toate generate in-house, deci nu au fost niciodată hibride. Primul fișier hibrid sosește în ziua în care un client trimite o factură exportată dintr-o foaie de calcul
Ce scriu de fapt Word și Excel
ISO 32000-1 descrie formatul (layout) cu referință hibridă în §7.5.8.4. O aplicație care dorește caracteristici PDF 1.5, cum ar fi fluxurile de obiecte (object streams), în timp ce permite totuși unui cititor PDF 1.4 să deschidă fișierul, scrie informațiile de referință încrucișată de două ori. Există un tabel clasic de referință încrucișată, rândurile ASCII cu lățime fixă care încheiau fiecare PDF până la versiunea 1.4, și există un flux de referință încrucișată care indexează restul. Trailerul secțiunii clasice poartă o intrare /XRefStm a cărei valoare este decalajul (offset) în octeți (byte) al acelui flux
Împărțirea muncii este deliberată. Obiectele la care trebuie să ajungă un cititor vechi, catalogul și arborele de pagini printre ele, sunt adresabile din tabelul clasic. Obiectele care au fost împăturite (folded) în fluxuri de obiecte comprimate sunt marcate ca libere în tabelul clasic, cu o intrare de tip f, astfel încât un cititor 1.4 sare direct peste ele și nu se împiedică niciodată de o structură pe care nu o poate analiza (parse). Locațiile lor reale trăiesc doar în fluxul de referință încrucișată. Semnătura unui astfel de fișier este finalul (tail) său: o secțiune clasică scurtă, adesea nimic mai mult decât xref urmat de un antet (header) de subsecțiune 0 0, al cărei trailer arată spre /XRefStm unde se află datele reale de recuperare
De ce un număr corect de pagini nu dovedește nimic
Deoarece catalogul și arborele de pagini sunt accesibile din tabelul clasic în mod intenționat, un încărcător (loader) care citește doar acel tabel găsește /Root, parcurge arborele de pagini și raportează numărul corect de pagini. Tot ce are nevoie un cititor vechi este prezent, așa că fișierul pare sănătos. Obiectele care au dispărut sunt cele împachetate (packed) în fluxurile de obiecte: dicționarele de câmpuri AcroForm, elementele de structură PDF etichetate (tagged-PDF), lunga coadă de dicționare mici care nu trebuiau niciodată să fie vizibile pentru un vizualizator tradițional (legacy viewer)
Nu observi golul până când ceva atinge acele obiecte, iar o resalvare completă le atinge pe toate. Parcurgerea documentului pentru a-l re-cripta sau rescrie este tocmai operațiunea care cere fiecare număr de obiect la rândul său, motiv pentru care simptomul iese la suprafață la momentul salvării mai degrabă decât la momentul încărcării, departe de cauza sa
Capcana este un detector care vede xref și se oprește
Modul ieftin de a decide cum este indexat un fișier este de a urmări startxref și de a inspecta primii octeți (bytes) la care arată. Cuvântul cheie xref înseamnă un tabel clasic; un obiect flux (stream object) înseamnă un flux de referință încrucișată. Acel test este corect pentru orice fișier care se angajează la o singură schemă. Este greșit pentru un fișier hibrid, al cărui startxref vizează o secțiune clasică cu unicul scop de a satisface cititorii vechi, în timp ce /XRefStm din trailerul acelei secțiuni este locul unde majoritatea documentului este de fapt indexată. Un detector care returnează "clasic" la primul xref pe care îl întâlnește nu citește niciodată /XRefStm, iar fiecare obiect care trăiește doar în flux devine invizibil
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // numărul este corect
// inspectează sau editează documentul încărcat aici
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // parcurge fiecare obiect
finally
Pdf.Free;
end;
end;
Cu detectorul cu ieșire timpurie (early-exit) la locul său, încărcarea arată bine și resalvarea este locul unde obiectele absente se anunță singure. Soluția (fix) nu este să citești mai mulți octeți (bytes) la început; este să recunoști trailerul hibrid și să urmărești /XRefStm înainte de a decide că fișierul este gata
Ordinea de îmbinare (Merge order) nu este negociabilă
Odată ce ambii indecși au fost citiți, ei pot fi combinați într-o singură direcție. Fluxul de referință încrucișată trebuie îmbinat primul, cu intrările clasice completate în jurul lui. Motivul este mica înșelăciune din inima formatului. Un fișier hibrid marchează obiectele sale comprimate ca libere în tabelul clasic astfel încât cititorii vechi să le ignore. Un încărcător (loader) care onorează o politică de tip primul văzut, primul servit (first-seen-wins) și citește tabelul clasic primul va înregistra acele numere de obiect ca libere, apoi va arunca (discard) intrările din flux care le localizează de fapt, pentru că sloturile sunt deja luate. Inversează ordinea și intrările de tip 2 din flux, fiecare reprezentând un număr de flux de obiecte plus un index, câștigă sloturile pe care sunt menite să le dețină, iar intrările clasice se așează în jurul lor
Aceeași disciplină protejează împotriva învierii (resurrecting) unui obiect șters de către o revizuire mai veche. Actualizările incrementale se leagă înapoi prin /Prev, și o intrare liberă de tip 0 este o santinelă (sentinel) care indică faptul că o secțiune mai recentă a retras un număr de obiect. Unei secțiuni ulterioare, mai vechi din lanț, nu trebuie să i se permită să suprascrie (overwrite) acea santinelă cu o locație expirată (stale). Tratează prima apariție ca autoritară pentru markerii liberi și obiectul șters rămâne șters; tratează-l cu neglijență și propria istorie a unui fișier reanimă (reanimates) conținutul pe care ultima revizuire l-a eliminat
Ce înseamnă acest lucru în HotPDF
Motorul rezolvă fișierele cu referință hibridă pentru tine, și face acest lucru pe fiecare cale care trebuie să analizeze (parse) datele de referință încrucișată. Încarcă un document cu LoadFromFile sau LoadFromStream, fă-ți modificările, și apelează SaveLoadedDocument; sau rulează o operațiune singulară (one-shot) cum ar fi EncryptFile care citește o intrare și scrie o ieșire. În oricare dintre cazuri, recuperarea citește /XRefStm, îmbină secțiunea de flux înaintea intrărilor clasice, și rezolvă obiectele care trăiesc în fluxuri înainte ca scrierea să le enumere. Calea de criptare AES-256 este locul unde problema s-a arătat prima dată, deoarece criptarea unui document rescrie fiecare obiect și prin urmare cere ca fiecare obiect să fi fost deja localizat
// Operațiune singulară (One-shot): citește intrarea hibridă, scrie o copie criptată AES-256
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Detaliul care merită reținut se află în amonte de API. Fișierele care sosesc din Word, Excel, PowerPoint și o listă lungă de conducte (pipelines) „Salvează ca PDF” sunt în mod curent hibride, astfel încât un încărcător pe care îl testezi (exercise) doar împotriva propriului rezultat generat (generator output) s-ar putea să nu întâlnească niciodată unul în testare. Alimentează-ți testele (fixtures) cu documente exportate din aplicații Office reale, nu doar cu fișiere pe care propriul cod le-a produs
Verificarea unui fișier pe care îl suspectezi
Două inspecții rezolvă problema rapid. Deschide fișierul într-o vizualizare hex și citește octeții de după ultimul startxref; un fișier hibrid arată o secțiune clasică scurtă a cărei dicționar trailer conține /XRefStm. Sau compară numărul de obiecte (object count) pe care o parsare completă îl raportează cu cel mai mare număr de obiect pe care /Size îl declară în trailer. Un decalaj mare înseamnă că obiectele se ascund în fluxuri pe care încărcătorul nu le-a deschis, ceea ce este același deficit (shortfall) care se transformă mai târziu într-un eșec la timpul de salvare (save-time)
Partea finală (tail) a unui export Excel tipic face prima verificare concretă. Totul după cuvântul cheie final xref este ASCII simplu, deci semnătura este lizibilă direct dintr-o vizualizare hex (decalaje ilustrative, adnotări adăugate)
xref
0 0 % subsecțiune clasică goală: niciun rând de fapt
trailer
<< /Size 216 % unul peste cel mai mare număr de obiect în uz
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % byte offset (decalaj octeți) al fluxului de referințe încrucișate
>>
startxref
88710 % indică secțiunea clasică de mai sus
%%EOF
Subsecțiunea 0 0 este indicatorul (tell): un tabel clasic cu zero intrări există doar pentru a purta trailerul, iar trailerul există în principal pentru a spune /XRefStm 87325. Un detector care se oprește la cuvântul cheie xref a văzut, în acest punct, un index al nimicului. Când preferi să scriptezi verificarea decât să o estimezi vizual, markerul se află întotdeauna în ultimii câțiva kiloocteți ai fișierului, deci o citire înapoi delimitată (bounded backward read) este suficientă
// Returnează offset-ul /XRefStm de la coada fișierului, sau -1 dacă
// markerul este absent (fișierul nu este hibrid, sau nu e deloc PDF)
function FindXRefStm(const FileName: string): Int64;
var
FS: TFileStream;
Tail: AnsiString;
Len, P: Integer;
begin
Result := -1;
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Len := 2048; // trailerul trăiește la coadă
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // citire delimitată înapoi: 2 KB max
SetLength(Tail, Len);
FS.ReadBuffer(Tail[1], Len);
finally
FS.Free;
end;
P := Pos(AnsiString('/XRefStm'), Tail);
if P = 0 then
Exit; // niciun marker hibrid la coadă
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // sare spațiile albe (whitespace) după cheie
Result := 0;
while (P <= Len) and (Tail[P] in ['0'..'9']) do
begin
Result := Result * 10 + Ord(Tail[P]) - Ord('0');
Inc(P);
end;
end;
// Utilizare: un rezultat nenegativ numește octetul de unde începe fluxul
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('fișier cu referință hibridă: resalvarea va necesita secțiunea /XRefStm');
Tratează proba (probe) ca pe un triaj, nu ca pe un analizor (parser): îți spune care fișiere dintr-un lot (batch) merită atenție înainte ca o sarcină de resalvare să ruleze, și nimic mai mult. Ceea ce un încărcător (loader) trebuie să facă apoi cu decalajul (offset) pe care îl găsește, urmând lanțul secțiunilor, îmbinând intrările fluxului (stream) înaintea celor clasice, respectând santinelele de intrare liberă, este explicat pas cu pas în articolul nostru însoțitor despre manipularea PDF-urilor cu referință hibridă din aplicațiile Office
Partea scriitorului în această poveste, felul în care fluxurile de obiecte și referințele încrucișate comprimate sunt produse în primul rând, este acoperită în articolul nostru despre fluxurile de obiecte și actualizările incrementale. Când fișierul hibrid în cauză este de asemenea foarte mare, tehnicile de încărcare din parcurgerea API-ului de Fișier Direct pentru fluxurile de lucru cu PDF-uri mari îți permit să îl inspectezi fără a citi tot fișierul în memorie. Ambele se potrivesc natural cu recuperarea descrisă aici, care se livrează ca parte a Componentei HotPDF pentru Delphi și C++Builder, alături de API-urile de încărcare, editare, criptare și semnare acoperite în altă parte pe acest blog