En PDF-leser starter ikke på begynnelsen av filen. Den starter på slutten. De siste få bytene inneholder adressen til alt annet, og en parser som ikke forstår den rekkefølgen, vil feiltolke formatet fra første linje. Så den mest nyttige måten å lære PDF på disk, er å lære det slik en leser gjør: halen først, deretter hoppe bakover til kartet, for så å løse opp objektene kartet peker på
Selve bytene er enkle nok å lese i en tekstredigerer når ingenting er komprimert. Et minimalt ensiders dokument som tegner "Hello, World!" får plass på under fem hundre byter, og hvert strukturelt element i formatet er synlig i det. Her er hele filen, med de fire delene markert:
%PDF-1.0 % Header
%âãÏÓ
1 0 obj % Body: the object sequence
<<
/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 % Cross-reference table
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
Fire deler, alltid i denne rekkefølgen nedover filen: en overskrift (header), en kropp (body) av objekter, en kryssreferansetabell (xref), og en avslutning (trailer). Haken er at du leser dem i nesten omvendt rekkefølge. ISO 32000-2 §7.5.1 legger ut den samme firedelte anatomien, og grunnen til bak-til-front-tilgangen er rent praktisk: en leser som hopper rett til objektet den trenger, er mye raskere enn en som skanner hver byte fra toppen, og den tilfeldige tilgangen er nøyaktig hva traileren og kryssreferansetabellen eksisterer for å gi
Overskriften er to linjer, og den andre er viktig
Den første linjen er %PDF-1.0. Prosenttegnet gjør den til en kommentar så langt som syntaksen angår, men lesere behandler den som filsignaturen og trekker ut versjonsnummeret fra den. Versjonshåndtering er løs i praksis. En leser bygget for PDF 2.0 vil gladelig åpne en fil som hevder 1.0, og de fleste lesere vil forsøke seg på en fil hvis erklærte versjon er feil, eller hvis versjonslinje er begravd litt inne i filen i stedet for ved byte null. Tallet er et hint om hvilke funksjoner man kan forvente, ikke en port
Den andre linjen er den folk sletter ved et uhell for deretter å bruke en ettermiddag på feilsøking. Det er også en kommentar, men innholdet er fire byter over ASCII 127. De eksisterer slik at alt som flytter filen i "tekstmodus" gjenkjenner den som binær og slutter å skrive om linjeavslutninger. En PDF bærer komprimerte strømmer hvis byter kan samsvare med en vognretur (carriage return) eller linjeskift ved en tilfeldighet; hvis et overføringsverktøy skriver om disse, samsvarer ikke lenger strømlengden registrert i ordboken med bytene på disken, og filen er korrupt. Høybyte-kommentaren er et førti år gammelt forsvar mot FTP i ASCII-modus, og den er fremdeles i hver fil et seriøst verktøy skriver fordi feilen den forhindrer er stille og total
Kroppen holder objektene, hver av dem nummerert
Alt som utgjør dokumentet, lever i kroppen som en flat sekvens av indirekte objekter. Hver og en åpner med to heltall og obj-nøkkelordet, inneholder innholdet sitt, og lukkes med endobj. Objekt 1 i eksempelet ovenfor er sidetre-noden: 1 0 obj, deretter en ordbok, deretter endobj. Det første heltallet er objektnummeret, det andre er generasjonsnummeret. Generasjon er nesten alltid null i en nyskrevet fil; den stiger bare når et objektnummer blir gjenbrukt på tvers av redigeringer, noe som er sjelden nok til at du kan behandle en generasjon ulik null som et tegn på at filen har vært gjennom inkrementelle oppdateringer. Innholdet mellom nøkkelordene er en ordbok her, skrevet mellom << og >>, men det kunne like gjerne vært et tall, en streng, en matrise eller en strøm
Det som gjør dette til en graf i stedet for en liste, er referansesymbolet 2 0 R. Det betyr "objekt 2, generasjon 0, uansett hvor det tilfeldigvis lever i filen." Sidetre-noden ovenfor inneholder ikke siden sin; den peker på objekt 2, som peker på ressursene og innholdsstrømmen sin med samme mekanisme. Kroppen er lagt ut i den rekkefølgen skriveren fant praktisk, og referansene syr den sammen til et tre forankret i katalogen. Posisjon i filen bærer ingen mening. Identitet kommer fra objektnummeret, og plassering kommer fra kryssreferansetabellen
Kryssreferansetabellen er en indeks over byteforskyvninger
Xref-tabellen er det som gjør objektnumre om til filposisjoner. Det er grunnen til at en leser kan åpne et tusensiders dokument og gjengi side 850 uten å tolke de 849 sidene før det. Hver oppføring registrerer nøyaktig hvor objektet begynner, talt i byter fra starten av filen:
xref
0 6 % 6 entries, starting at object 0
0000000000 65535 f % entry 0: head of the free list
0000000015 00000 n % object 1 begins at byte 15
0000000074 00000 n % object 2 begins at byte 74
0000000192 00000 n % object 3 begins at byte 192
0000000291 00000 n % object 4 begins at byte 291
0000000409 00000 n % object 5 begins at byte 409
Den faste bredden er bevisst. Hver oppføring er nøyaktig tjue byter: en tisifret forskyvning, et mellomrom, en femsifret generasjon, et mellomrom, en entegns type, og et tobytes linjeskift. Fordi radene er ensartede, kan en leser indeksere rett til oppføringen for objekt n med aritmetikk i stedet for skanning, så tabellen som gir tilfeldig tilgang til kroppen, er i seg selv tilfeldig tilgjengelig. 0 6-linjen er en underseksjonsoverskrift: den sier at de neste oppføringene beskriver seks objekter som starter på nummer 0
Objekt 0 er spesielt og alltid til stede. Typen er f for fri, generasjonen er 65535, og den leder den lenkede listen over ledige objektnumre. I en fil som aldri har blitt redigert, er fri-listen bare denne ene oppføringen, en formalitet. Den gjør nytte for seg under inkrementelle oppdateringer, når sletting av et objekt legger nummeret til den listen slik at en senere redigering kan kreve det tilbake. De andre oppføringene er av type n for i-bruk, og det tisifrede nummeret deres er forskyvningen du ville søke til for å lese det objektets definisjon
Traileren er inngangspunktet, og den sitter på slutten
Traileren er det første en leser faktisk konsumerer, selv om den skrives sist. En parser åpner filen, søker til slutten og går bakover på jakt etter %%EOF. Rett over den sitter startxref etterfulgt av et enkelt tall, og det tallet er byteforskyvningen til xref-nøkkelordet. Med den hopper leseren direkte til kryssreferansetabellen uten å ha skannet et eneste objekt:
trailer
<<
/Root 5 0 R % the document catalog
/Size 6 % one more than the highest object number
>>
startxref
459 % byte offset of the xref table
%%EOF
Trailer-ordboken bærer de to verdiene en leser trenger før den kan gjøre noe annet. /Root peker på dokumentkatalogen, objekt 5 her, som er toppen av objektgrafen og ruten til sidetreet. /Size er antallet oppføringer kryssreferansetabellen bør inneholde, som er én mer enn det høyeste objektnummeret på grunn av den frie oppføringen på plass null. Fra %%EOF faller hele lesesekvensen på plass: finn markøren, les startxref for å finne tabellen, last tabellen for å lære hvor hvert objekt lever, les /Root for å finne katalogen, og løs opp objekter ved behov derfra. Overskriften, som sitter på toppen, konsulteres knapt før sent. Kartet i bunnen er hva leseren trenger først
Inkrementell oppdatering legger til et nytt kart i stedet for å skrive om
Den hale-først-designen lønner seg når en fil endres. En PDF kan redigeres uten å skrive om noen av bytene som allerede er på disken. Nye og endrede objekter legges til på slutten, etterfulgt av en fersk kryssreferanseseksjon og en fersk trailer, og den originale filen under blir stående urørt. Det ene nye bokføringselementet er en /Prev-oppføring i den nye traileren, som holder byteforskyvningen til den forrige kryssreferansetabellen:
% ... original file, unchanged, ends here ...
6 0 obj % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj
xref % a second xref section, for the new object only
6 1
0000000612 00000 n
trailer
<<
/Root 5 0 R
/Size 7
/Prev 459 % byte offset of the earlier xref table
>>
startxref
680 % offset of this new xref section
%%EOF
En leser starter fortsatt ved den endelige %%EOF, følger fortsatt startxref til den nyeste tabellen, men følger nå /Prev-kjeden bakover til eldre tabeller, og slår dem sammen slik at den nyeste oppføringen for ethvert objektnummer vinner. Kryssreferanseseksjonene danner en lenket liste nedover gjennom filen, der hver og en overstyrer den forrige for objektene den berører. Et objekt som en redigering erstattet, eksisterer fortsatt fysisk på sin gamle forskyvning; det er bare ikke lenger tilgjengelig, fordi en senere xref-oppføring peker et nyere sted
Dette er mekanismen som gjør signerte PDF-er verifiserbare. En digital signatur dekker et byteområde av filen, og fordi en inkrementell oppdatering bare legger til, flytter aldri de signerte bytene seg. Signaturen validerer fortsatt mot det opprinnelige området mens senere revisjoner sitter utenfor det, hver med sin egen xref og trailer. Det er også grunnen til at en PDF kan bære gjenopprettbar historikk: hvert erstattet objekt er fortsatt på disk under en tidligere kryssreferanseseksjon, som er en funksjon for versjonssporing og et ansvar for alle som trodde "slett" betydde at bytene var borte
Kostnaden er vekst. Hver redigering legger til; ingenting gjenvinnes på stedet, så en fil revidert mange ganger samler opp døde objekter og en lang kjede av xref-seksjoner. Løsningen er en full omskriving: last dokumentet og lagre det friskt, noe som nummererer de overlevende objektene på nytt, kaster de utilgjengelige, og sender ut en enkelt ren kryssreferansetabell. De to strategiene veies direkte opp mot hverandre. Tilføyelse er raskt og bevarer signaturer og historikk; omskriving er tregere og forkaster begge deler, i bytte mot en kompakt fil
Lesing av de fire delene i praksis
Å kjenne til oppsettet er nok til å feilsøke de fleste "denne filen vil ikke åpne"-problemer for hånd. Hvis en leser avviser en PDF, er de vanlige synderne i de to endene, ikke i midten. En avkortet nedlasting mister traileren, så startxref eller %%EOF mangler og leseren har ikke noe inngangspunkt; tolerante lesere faller tilbake til å skanne hele filen for å bygge opp igjen xref, som er nøyaktig den trege veien tabellen var ment å unngå. En feilslått overføring i tekstmodus ødelegger strømbyter eller forskyvningene slutter å stemme med virkeligheten, og objekter lastes fra feil posisjon. Når forskyvningene i tabellen ikke lenger peker på ekte obj-nøkkelord, er filen strukturelt ødelagt selv om hvert objekt er individuelt fint
For ny kode er lærdommen av oppsettet å la et bibliotek eie bytebokføringen. Forskyvningene i kryssreferansetabellen må stemme overens med de faktiske posisjonene til hvert objekt på byten, traileren må peke på riktig tabell, og inkrementelle oppdateringer må lenkes riktig gjennom /Prev. En innfødt komponent som HotPDF-komponenten for Delphi og C++Builder håndterer alt dette når den skriver en fil, inkludert valget mellom å legge til en inkrementell revisjon og skrive om en kompakt en. Hvis du vil se den samme strukturen bygget opp fra ingenting i stedet for dissekert, går ledsagerartikkelen om å bygge et PDF-dokument fra bunnen av gjennom utsendelse av overskrift, objekter, xref og trailer i rekkefølge