Un cititor PDF nu pornește de la începutul fișierului. Pornește de la final. Ultimii octeți conțin adresa tuturor celorlalte, iar un parser care nu înțelege această ordine va citi greșit formatul încă de la primul rând. Așa că cel mai util mod de a învăța PDF-ul așa cum stă pe disc este să îl învățați așa cum face un cititor: mai întâi coada, apoi un salt înapoi la hartă, apoi rezolvarea obiectelor pe care le indică harta
Octeții în sine sunt destul de lizibili într-un editor de text atunci când nimic nu este comprimat. Un document minimal de o pagină care desenează „Hello, World!” încape în mai puțin de cinci sute de octeți, iar fiecare element structural al formatului este vizibil în el. Iată fișierul întreg, cu cele patru părți marcate:
%PDF-1.0 % Antet
%âãÏÓ
1 0 obj % Corp: secvența de obiecte
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj
2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj
3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj
4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream
endobj
5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj
xref % Tabel de referințe încrucișate
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n
trailer % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF
Patru părți, mereu în această ordine de-a lungul fișierului: un antet, un corp de obiecte, un tabel de referințe încrucișate și un trailer. Șmecheria este că le citiți aproape în ordine inversă. ISO 32000-2 §7.5.1 descrie aceeași anatomie în patru părți, iar motivul accesului dinspre coadă este pur practic: un cititor care sare direct la obiectul de care are nevoie este mult mai rapid decât unul care parcurge fiecare octet de sus în jos, iar acest acces aleatoriu este exact ce există să ofere trailerul și tabelul de referințe încrucișate
Antetul are două rânduri, iar al doilea contează
Primul rând este %PDF-1.0. Semnul procent îl transformă în comentariu din punctul de vedere al sintaxei, dar cititorii îl tratează ca semnătură a fișierului și extrag din el numărul de versiune. Tratarea versiunii este laxă în practică. Un cititor construit pentru PDF 2.0 va deschide fără probleme un fișier care pretinde 1.0, iar majoritatea cititorilor vor încerca un fișier a cărui versiune declarată este greșită sau al cărui rând de versiune este îngropat puțin mai adânc în fișier în loc să stea la octetul zero. Numărul este un indiciu despre funcțiile la care să vă așteptați, nu o barieră
Al doilea rând este cel pe care lumea îl șterge din greșeală și apoi petrece o după-amiază depanând. Este tot un comentariu, dar încărcătura lui este de patru octeți peste ASCII 127. Ei există pentru ca orice mută fișierul în „mod text” să îl recunoască drept binar și să nu îi mai rescrie terminatorii de rând. Un PDF poartă fluxuri comprimate ai căror octeți pot corespunde din întâmplare unui retur de car sau unui salt de rând; dacă un instrument de transfer le rescrie, lungimea fluxului consemnată în dicționar nu mai corespunde octeților de pe disc, iar fișierul este corupt. Comentariul cu octeți înalți este o apărare veche de patruzeci de ani împotriva FTP-ului în mod ASCII și se află în continuare în fiecare fișier scris de un instrument serios, pentru că defecțiunea pe care o previne este tăcută și totală
Corpul conține obiectele, fiecare numerotat
Tot ce alcătuiește documentul trăiește în corp, ca o secvență plată de obiecte indirecte. Fiecare se deschide cu două numere întregi și cuvântul-cheie obj, își conține conținutul și se închide cu endobj. Obiectul 1 din exemplul de mai sus este nodul arborelui de pagini: 1 0 obj, apoi un dicționar, apoi endobj. Primul întreg este numărul obiectului, al doilea este numărul de generație. Generația este aproape întotdeauna zero într-un fișier proaspăt scris; crește doar când un număr de obiect este reutilizat între editări, ceea ce este destul de rar cât să puteți trata o generație nenulă drept semn că fișierul a trecut prin actualizări incrementale. Conținutul dintre cuvintele-cheie este aici un dicționar, scris între << și >>, dar ar putea la fel de bine să fie un număr, un șir, un tablou sau un flux
Ceea ce face din asta un graf, nu o listă, este simbolul de referință 2 0 R. El înseamnă „obiectul 2, generația 0, oriunde s-ar afla el în fișier”. Nodul arborelui de pagini de mai sus nu își conține pagina; el indică obiectul 2, care indică prin același mecanism resursele și fluxul lui de conținut. Corpul este dispus în orice ordine i-a convenit celui care a scris fișierul, iar referințele îl coase într-un arbore cu rădăcina în catalog. Poziția în fișier nu poartă niciun înțeles. Identitatea vine din numărul obiectului, iar locația vine din tabelul de referințe încrucișate
Tabelul de referințe încrucișate este un index de offseturi în octeți
Tabelul xref este cel care transformă numerele de obiect în poziții din fișier. El este motivul pentru care un cititor poate deschide un document de o mie de pagini și randa pagina 850 fără să le parseze pe cele 849 dinaintea ei. Fiecare intrare consemnează exact unde începe obiectul ei, numărat în octeți de la începutul fișierului:
xref
0 6 % 6 intrări, începând de la obiectul 0
0000000000 65535 f % intrarea 0: capul listei de libere
0000000015 00000 n % obiectul 1 începe la octetul 15
0000000074 00000 n % obiectul 2 începe la octetul 74
0000000192 00000 n % obiectul 3 începe la octetul 192
0000000291 00000 n % obiectul 4 începe la octetul 291
0000000409 00000 n % obiectul 5 începe la octetul 409
Lățimea fixă este intenționată. Fiecare intrare are exact douăzeci de octeți: un offset de zece cifre, un spațiu, o generație de cinci cifre, un spațiu, un caracter de tip și un terminator de rând pe doi octeți. Pentru că rândurile sunt uniforme, un cititor poate indexa direct la intrarea obiectului n prin aritmetică, nu prin parcurgere, așa că tabelul care oferă acces aleatoriu la corp este el însuși accesibil aleatoriu. Rândul 0 6 este un antet de subsecțiune: el spune că intrările următoare descriu șase obiecte începând de la numărul 0
Obiectul 0 este special și mereu prezent. Tipul lui este f, de la liber, generația lui este 65535, iar el se află în fruntea listei înlănțuite de numere de obiect libere. Într-un fișier care nu a fost niciodată editat, lista de libere este doar această singură intrare, o formalitate. Ea își merită locul în timpul actualizărilor incrementale, când ștergerea unui obiect adaugă numărul lui în acea listă, ca o editare ulterioară să îl poată recupera. Celelalte intrări sunt de tip n, adică în uz, iar numărul lor de zece cifre este offsetul la care v-ați poziționa pentru a citi definiția acelui obiect
Trailerul este punctul de intrare și stă la final
Trailerul este primul lucru pe care îl consumă efectiv un cititor, deși este scris ultimul. Un parser deschide fișierul, se poziționează la final și merge înapoi căutând %%EOF. Imediat deasupra lui stă startxref urmat de un singur număr, iar acel număr este offsetul în octeți al cuvântului-cheie xref. Cu el, cititorul sare direct la tabelul de referințe încrucișate fără să fi parcurs vreun obiect:
trailer
<<
/Root 5 0 R % catalogul documentului
/Size 6 % cu unu mai mult decât cel mai mare număr de obiect
>>
startxref
459 % offsetul în octeți al tabelului xref
%%EOF
Dicționarul trailerului poartă cele două valori de care are nevoie un cititor înainte să poată face orice altceva. /Root indică spre catalogul documentului, aici obiectul 5, care este vârful grafului de obiecte și drumul spre arborele de pagini. /Size este numărul de intrări pe care ar trebui să le conțină tabelul de referințe încrucișate, adică cu unu mai mult decât cel mai mare număr de obiect, din cauza intrării libere de pe poziția zero. De la %%EOF decurge întreaga secvență de citire: găsiți marcajul, citiți startxref ca să localizați tabelul, încărcați tabelul ca să aflați unde se află fiecare obiect, citiți /Root ca să găsiți catalogul și rezolvați obiectele la cerere de acolo. Antetul, aflat sus, abia dacă este consultat, și asta târziu. Harta de la bază este cea de care are nevoie cititorul mai întâi
Actualizarea incrementală adaugă o a doua hartă în loc să rescrie
Acest design orientat spre coadă dă roade când fișierul se schimbă. Un PDF poate fi editat fără rescrierea vreunuia dintre octeții aflați deja pe disc. Obiectele noi și cele modificate se adaugă la final, urmate de o secțiune proaspătă de referințe încrucișate și de un trailer proaspăt, iar fișierul original de dedesubt rămâne neatins. Singura evidență nouă este o intrare /Prev în noul trailer, care păstrează offsetul în octeți al tabelului anterior de referințe încrucișate:
% ... fișierul original, neschimbat, se termină aici ...
6 0 obj % un obiect adăugat de această editare
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj
xref % o a doua secțiune xref, doar pentru obiectul nou
6 1
0000000612 00000 n
trailer
<<
/Root 5 0 R
/Size 7
/Prev 459 % offsetul în octeți al tabelului xref anterior
>>
startxref
680 % offsetul acestei noi secțiuni xref
%%EOF
Un cititor tot pornește de la ultimul %%EOF, tot urmează startxref spre cel mai recent tabel, dar acum urmează lanțul /Prev înapoi spre tabelele mai vechi, îmbinându-le astfel încât cea mai nouă intrare pentru orice număr de obiect să câștige. Secțiunile de referințe încrucișate formează o listă înlănțuită de-a lungul fișierului, fiecare suprascriind-o pe cea dinaintea ei pentru obiectele pe care le atinge. Un obiect pe care o editare l-a înlocuit continuă să existe fizic la vechiul lui offset; pur și simplu nu mai este accesibil, pentru că o intrare xref ulterioară indică spre ceva mai nou
Acesta este mecanismul care face verificabile PDF-urile semnate. O semnătură digitală acoperă un interval de octeți din fișier, iar pentru că o actualizare incrementală doar adaugă la final, octeții semnați nu se mută niciodată. Semnătura se validează în continuare față de intervalul original, în timp ce reviziile ulterioare stau dincolo de el, fiecare cu propriul xref și trailer. Tot de aceea un PDF poate purta un istoric recuperabil: fiecare obiect înlocuit se află încă pe disc, sub o secțiune de referințe încrucișate anterioară, ceea ce este o funcție utilă pentru urmărirea versiunilor și o vulnerabilitate pentru oricine a crezut că „ștergere” înseamnă că octeții au dispărut
Prețul este creșterea. Fiecare editare adaugă; nimic nu este recuperat pe loc, așa că un fișier revizuit de multe ori acumulează obiecte moarte și un lanț lung de secțiuni xref. Remediul este o rescriere completă: încărcați documentul și salvați-l din nou, ceea ce renumerotează obiectele supraviețuitoare, elimină pe cele inaccesibile și emite un singur tabel curat de referințe încrucișate. Cele două strategii se compensează direct una pe alta. Adăugarea la final este rapidă și păstrează semnăturile și istoricul; rescrierea este mai lentă și le aruncă pe amândouă, în schimbul unui fișier compact
Citirea celor patru părți în practică
Cunoașterea dispunerii este suficientă pentru a depana manual majoritatea problemelor de tip „fișierul acesta nu se deschide”. Dacă un cititor respinge un PDF, vinovații obișnuiți se află la cele două capete, nu la mijloc. O descărcare trunchiată pierde trailerul, așa că startxref sau %%EOF lipsește, iar cititorul nu are punct de intrare; cititorii toleranți revin la parcurgerea întregului fișier pentru a reconstrui xref-ul, adică exact drumul lent pe care tabelul trebuia să îl evite. Un transfer ratat în mod text corupe octeții fluxurilor sau offseturile încetează să mai corespundă realității, iar obiectele se încarcă de la poziția greșită. Când offseturile din tabel nu mai indică spre cuvinte-cheie obj reale, fișierul este stricat structural chiar dacă fiecare obiect în parte este în regulă
Pentru cod nou, lecția acestei dispuneri este să lăsați o bibliotecă să dețină evidența octeților. Offseturile din tabelul de referințe încrucișate trebuie să corespundă la octet pozițiilor reale ale fiecărui obiect, trailerul trebuie să indice spre tabelul corect, iar actualizările incrementale trebuie să se înlănțuie corect prin /Prev. O componentă nativă precum HotPDF Delphi Component pentru Delphi și C++Builder se ocupă de toate acestea când scrie un fișier, inclusiv de alegerea între adăugarea unei revizii incrementale și rescrierea uneia compacte. Dacă vreți să vedeți aceeași structură construită de la zero în loc de disecată, articolul însoțitor despre construirea unui document PDF de la zero parcurge emiterea antetului, a obiectelor, a xref-ului și a trailerului, în ordine