Åpne en PDF som Microsoft Word eller Excel produserte, bla gjennom den, og ingenting ser uvanlig ut. Last den inn i et Delphi-program, les sidetallet tilbake, og tallet er riktig. Lagre den deretter på nytt med kryptering slått på, og jobben mislykkes med en EListError, eller resultatet åpnes med en advarsel om en ødelagt kryssreferanse. Filen var aldri korrupt. Det er en hybrid-referansefil, og selve strukturen som lar en femten år gammel fremviser åpne den, er strukturen som beseirer en laster som slutter å lese for tidlig
Dette er en av de vanligste måtene en PDF-rørledning (pipeline) som besto alle interne tester møter en fil den ikke kan tur-retur-behandle (round-trip). Inndataene ble alle generert internt, så de var aldri hybride. Den første hybridfilen ankommer den dagen en kunde videresender en faktura eksportert fra et regneark
Hva Word og Excel faktisk skriver
ISO 32000-1 beskriver hybrid-referanse-layouten i §7.5.8.4. En applikasjon som ønsker PDF 1.5-funksjoner som objektstrømmer (object streams), samtidig som den lar en PDF 1.4-leser åpne filen, skriver kryssreferanseinformasjonen to ganger. Det er en klassisk kryssreferansetabell, med fastbredde ASCII-rader som avsluttet hver PDF opp til versjon 1.4, og det er en kryssreferansestrøm som indekserer resten. Traileren til den klassiske seksjonen har en /XRefStm-oppføring hvis verdi er byteforskyvningen (byte offset) til den strømmen
Arbeidsdelingen er bevisst. Objekter en gammel leser må nå, katalogen (catalog) og sidetreet (page tree) blant dem, kan adresseres fra den klassiske tabellen. Objekter som ble brettet inn i komprimerte objektstrømmer er merket som ledige i den klassiske tabellen, med en type f-oppføring, slik at en 1.4-leser hopper rett forbi dem og aldri snubler over en struktur den ikke kan analysere. Deres virkelige plasseringer finnes bare i kryssreferansestrømmen. Signaturen til en slik fil er dens hale: en kort klassisk seksjon, ofte ingenting mer enn xref etterfulgt av et 0 0 underseksjonsoverskrift, hvis trailer peker på /XRefStm der de faktiske gjenopprettingsdataene sitter
Hvorfor et riktig sidetall ikke beviser noe
Fordi katalogen og sidetreet er tilgjengelige fra den klassiske tabellen med vilje, finner en laster som bare leser den tabellen /Root, går sidetreet og rapporterer riktig antall sider. Alt en gammel leser trenger er til stede, så filen virker sunn. Objektene som forsvant, er de som ble pakket inn i objektstrømmer: AcroForm-feltordbøker, tagged-PDF-strukturelementer, den lange halen av små ordbøker som aldri måtte være synlige for en eldre visningsapplikasjon
Du merker ikke gapet før noe berører disse objektene, og en full ny lagring berører dem alle. Å gå gjennom dokumentet for å kryptere det på nytt eller skrive det om, er nettopp operasjonen som ber om hvert objektnummer i tur og orden, noe som er grunnen til at symptomet dukker opp ved lagringstidspunktet i stedet for lastetidspunktet, langt borte fra årsaken
Fellen er en detektor som ser xref og stopper
Den billige måten å avgjøre hvordan en fil er indeksert på, er å følge startxref og inspisere de første bytene den peker på. Nøkkelordet xref betyr en klassisk tabell; et strømobjekt betyr en kryssreferansestrøm. Denne testen er riktig for enhver fil som forplikter seg til én ordning. Det er feil for en hybrid fil, hvis startxref sikter på en klassisk seksjon utelukkende for å tilfredsstille gamle lesere, mens /XRefStm i den seksjonens trailer er der mesteparten av dokumentet faktisk er indeksert. En detektor som returnerer "klassisk" på den første xref den møter, leser aldri /XRefStm, og hvert objekt som bare lever i strømmen, blir usynlig
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // tellingen er riktig
// inspiser eller rediger det innlastede dokumentet her
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // går gjennom hvert objekt
finally
Pdf.Free;
end;
end;
Med den tidlige utgangsdetektoren på plass ser lastingen bra ut, og nylagringen er der de fraværende objektene kunngjør seg selv. Fiksen er ikke å lese flere byter ved starten; det er å gjenkjenne den hybride traileren og følge /XRefStm før man beslutter at filen er ferdig
Flette-rekkefølge er ikke oppe til forhandling
Når begge indeksene har blitt lest, kan de slås sammen bare i én retning. Kryssreferansestrømmen må flettes først, med de klassiske oppføringene fylt ut rundt den. Årsaken er det lille bedraget i hjertet av formatet. En hybrid fil markerer sine komprimerte objekter som ledige i den klassiske tabellen, slik at gamle lesere ignorerer dem. En laster som hedrer et "først-sett-vinner"-prinsipp (first-seen-wins) og leser den klassiske tabellen først, vil registrere disse objektnumrene som ledige, og deretter forkaste strømoppføringene som faktisk lokaliserer dem, fordi sporene (slots) allerede er tatt. Reverser rekkefølgen og type 2-oppføringene fra strømmen, hver ett objekt-strøm-nummer pluss en indeks, vinner sporene de er ment å eie, og de klassiske oppføringene slår seg ned rundt dem
Den samme disiplinen garderer seg mot at en eldre revisjon gjenoppliver et slettet objekt. Inkrementelle oppdateringer lenker bakover gjennom /Prev, og en type 0 ledig-oppføring er en vaktpost (sentinel) om at en nyere seksjon har pensjonert et objektnummer. En senere, eldre seksjon i kjeden må ikke tillates å overskrive den vaktposten med en foreldet plassering. Behandle først-sett som autoritativ for ledig-markører, og det slettede objektet forblir slettet; behandle det skjødesløst, og filens egen historie gjenoppliver innhold som den siste revisjonen fjernet
Hva dette betyr i HotPDF
Motoren løser hybrid-referansefiler for deg, og den gjør det på hver bane som må analysere kryssreferansedataene. Last et dokument med LoadFromFile eller LoadFromStream, gjør endringene dine, og kall SaveLoadedDocument; eller kjør en engangsoperasjon (one-shot operation) som EncryptFile som leser en inndata og skriver en utdata. Uansett leser gjenopprettingen /XRefStm, slår sammen strømseksjonen foran de klassiske oppføringene, og løser objektene som lever i strømmer før skriveoperasjonen lister dem opp. AES-256-krypteringsbanen er der problemet først viste seg, fordi kryptering av et dokument skriver om hvert objekt, og krever dermed at hvert objekt allerede har blitt funnet
// Engangs (one-shot): les den hybride inndataen, skriv en AES-256-kryptert kopi
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Detaljen som er verdt å ta med seg, sitter oppstrøms for API-et. Filer som ankommer fra Word, Excel, PowerPoint, og en lang liste av "Lagre som PDF"-rørledninger er rutinemessig hybride, så en laster du kun utøver mot din egen generator-utdata vil kanskje aldri møte en i testing. Så oppsettene (fixtures) dine med dokumenter eksportert fra ekte Office-applikasjoner, ikke bare med filer koden din selv produserte
Sjekke en fil du mistenker
To inspeksjoner avgjør spørsmålet raskt. Åpne filen i en hex-visning og les bytene etter den siste startxref; en hybrid fil viser en kort klassisk seksjon hvis trailer-ordbok inneholder /XRefStm. Eller sammenlign objekttellingen som en fullstendig analyse rapporterer mot det høyeste objektnummeret som /Size erklærer i traileren. Et stort gap betyr at objekter gjemmer seg i strømmer lasteren ikke har åpnet, som er den samme mangelen som blir til en feil ved lagring senere
Halen til en typisk Excel-eksport gjør den første sjekken konkret. Alt etter det siste xref-nøkkelordet er ren ASCII, så signaturen er lesbar rett ut av en hex-visning (forskyvninger (offsets) illustrative, anmerkninger (annotations) lagt til)
xref
0 0 % tom klassisk underseksjon: ingen rader i det hele tatt
trailer
<< /Size 216 % én forbi det høyeste objektnummeret i bruk
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % byteforskyvning av kryssreferansestrømmen
>>
startxref
88710 % peker på den klassiske seksjonen over
%%EOF
0 0-underseksjonen er avsløringen: en klassisk tabell med null oppføringer eksisterer utelukkende for å bære traileren, og traileren eksisterer hovedsakelig for å si /XRefStm 87325. En detektor som stopper ved nøkkelordet xref har, på dette punktet, sett en indeks av ingenting. Når du heller vil scripte sjekken enn å se på den manuelt, sitter markøren alltid innenfor de siste par kilobytene av filen, så en begrenset baklengs lesing (bounded backward read) er nok
// Returnerer /XRefStm offset fra filens hale, eller -1 hvis
// markøren er fraværende (filen er ikke hybrid, eller ikke en PDF overhodet)
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; // traileren lever i halen
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // begrenset baklengs lesing: maks 2 KB
SetLength(Tail, Len);
FS.ReadBuffer(Tail[1], Len);
finally
FS.Free;
end;
P := Pos(AnsiString('/XRefStm'), Tail);
if P = 0 then
Exit; // ingen hybrid markør i halen
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // hopp over mellomrom etter nøkkelen
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;
// Bruk: et ikke-negativt resultat navngir byten der strømmen starter
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('hybrid-referansefil: nylagring vil trenge /XRefStm-seksjonen');
Behandle sonden som en triage (sortering), ikke som en parser: den forteller deg hvilke filer i en batch som fortjener oppmerksomhet før en nylagringsjobb kjører, og ingenting mer. Hva en laster da må gjøre med forskyvningen (offset) den finner, ved å følge seksjonskjeden, flette strømoppføringene foran de klassiske, og respektere ledig-oppføring-vaktposter, blir gjennomgått trinn for trinn i vår tilhørende artikkel om håndtering av hybrid-referanse-PDF-er fra Office-applikasjoner
Skriverens side av denne historien, hvordan objektstrømmer og komprimerte kryssreferanser produseres i utgangspunktet, er dekket i vår artikkel om objektstrømmer og inkrementelle oppdateringer. Når hybridfilen det er snakk om også er veldig stor, lar innlastingsteknikkene i gjennomgangen av Direct File API for store PDF-arbeidsflyter deg inspisere den uten å lese hele greia inn i minnet. Begge pares naturlig med gjenopprettingen som er beskrevet her, som leveres som en del av HotPDF-komponenten for Delphi og C++Builder ved siden av innlasting, redigering, kryptering og signerings-API-ene som dekkes andre steder på denne bloggen