Åbn en PDF, som Microsoft Word eller Excel har produceret, blad gennem den, og intet ser usædvanligt ud. Indlæs den i et Delphi-program, læs sideantallet tilbage, og tallet er rigtigt. Gen-gem (re-save) den derefter med kryptering slået til, og jobbet fejler med en EListError, eller outputtet åbner med en advarsel om beskadiget krydsreference (cross-reference). Filen var aldrig korrupt. Det er en hybrid-reference-fil, og netop den struktur, der lader en femten år gammel fremviser åbne den, er den struktur, der besejrer en loader, som stopper med at læse for tidligt
Dette er en af de mest almindelige måder, hvorpå en PDF-pipeline, der bestod enhver intern test, møder en fil, den ikke kan round-trippe. Inputtene blev alle genereret internt (in-house), så de var aldrig hybride. Den første hybridfil ankommer den dag, en kunde videresender en faktura eksporteret fra et regneark
Hvad Word og Excel faktisk skriver
ISO 32000-1 beskriver hybrid-reference-layoutet i §7.5.8.4. En applikation, der ønsker PDF 1.5-funktioner såsom objektstrømme (object streams), og samtidig vil lade en PDF 1.4-læser åbne filen, skriver krydsreferenceoplysningerne (cross-reference information) to gange. Der er en klassisk krydsreferencetabel, de fastbredde ASCII-rækker, der afsluttede hver PDF op til version 1.4, og der er en krydsreferencestrøm, der indekserer resten. Traileren i den klassiske sektion bærer en /XRefStm-post, hvis værdi er byte-offsettet for den strøm
Arbejdsdelingen er bevidst. Objekter, som en gammel læser skal nå, herunder kataloget og sidetræet, kan adresseres fra den klassiske tabel. Objekter, der blev foldet ind i komprimerede objektstrømme, er markeret som frie (free) i den klassiske tabel med en type f-post, så en 1.4-læser springer direkte forbi dem og aldrig falder over en struktur, den ikke kan parse. Deres virkelige placeringer lever kun i krydsreferencestrømmen. Signaturen på en sådan fil er dens hale (tail): en kort klassisk sektion, ofte intet mere end xref efterfulgt af en 0 0 undersektion-header (subsection header), hvis trailer peger på den /XRefStm, hvor de faktiske gendannelsesdata sidder
Hvorfor et korrekt sideantal intet beviser
Fordi kataloget og sidetræet er tilgængelige fra den klassiske tabel med vilje, finder en loader, der kun læser den tabel, /Root, gennemgår sidetræet og rapporterer det rigtige antal sider. Alt, hvad en gammel læser har brug for, er til stede, så filen fremstår sund. De objekter, der forsvandt, er dem, der er pakket ind i objektstrømme: AcroForm-feltordbøger (field dictionaries), tagged-PDF-strukturelementer, den lange hale af små ordbøger, der aldrig behøvede at være synlige for en ældre (legacy) fremviser
Du bemærker ikke hullet, før noget rører ved disse objekter, og en fuld gen-gemning (resave) rører ved dem alle. At gennemgå dokumentet for at gen-kryptere (re-encrypt) eller genskrive det, er præcis den operation, der beder om hvert objektnummer efter tur, hvilket er grunden til, at symptomet dukker op på gem-tidspunktet snarere end indlæsningstidspunktet, langt fra dets årsag
Fælden er en detektor, der ser xref og stopper
Den billige måde at beslutte, hvordan en fil er indekseret, er at følge startxref og inspicere de første bytes, den peger på. Nøgleordet xref betyder en klassisk tabel; et stream-objekt betyder en krydsreferencestrøm. Den test er korrekt for enhver fil, der forpligter sig til én ordning (scheme). Den er forkert for en hybridfil, hvis startxref sigter mod en klassisk sektion med det ene formål at tilfredsstille gamle læsere, mens /XRefStm i den sektions trailer er der, hvor det meste af dokumentet faktisk er indekseret. En detektor, der returnerer "classic" ved den første xref, den møder, læser aldrig /XRefStm, og hvert objekt, der kun lever i strømmen, bliver usynligt
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Med den tidlige-afslutning-detektor (early-exit detector) på plads ser indlæsningen fin ud, og gen-gemningen er der, hvor de fraværende objekter melder deres ankomst. Løsningen er ikke at læse flere bytes i starten; det er at genkende hybrid-traileren og følge /XRefStm, før man beslutter, at filen er færdig
Flette-rækkefølge (Merge order) er ikke til forhandling
Når begge indekser er blevet læst, kan de kun kombineres i én retning. Krydsreferencestrømmen skal flettes først, med de klassiske poster udfyldt omkring den. Grunden er det lille bedrag i hjertet af formatet. En hybridfil markerer sine komprimerede objekter som frie i den klassiske tabel, så gamle læsere ignorerer dem. En loader, der respekterer en først-set-vinder (first-seen-wins) politik og læser den klassiske tabel først, vil registrere disse objektnumre som frie og derefter kassere strøm-posterne (stream entries), der faktisk lokaliserer dem, fordi pladserne (slots) allerede er taget. Vend rækkefølgen om, og type 2-posterne fra strømmen, der hver især er et objektstrøm-nummer plus et indeks, vinder de pladser, de er beregnet til at eje, og de klassiske poster lægger sig omkring dem
Den samme disciplin beskytter mod en ældre revision, der genopliver (resurrecting) et slettet objekt. Trinvise opdateringer (Incremental updates) kædes baglæns gennem /Prev, og en type 0 fri post (free entry) er en vagtpost (sentinel) om, at en nyere sektion har pensioneret et objektnummer. En senere, ældre sektion i kæden må ikke have lov til at overskrive denne vagtpost med en forældet placering (stale location). Behandl først-set (first-seen) som autoritativ for frie markører, og det slettede objekt forbliver slettet; behandl det skødesløst, og en fils egen historie genopliver indhold, som den seneste revision fjernede
Hvad dette betyder i HotPDF
Motoren løser hybrid-reference-filer for dig, og den gør det på enhver sti, der skal parse krydsreferencedataene. Indlæs et dokument med LoadFromFile eller LoadFromStream, foretag dine ændringer, og kald SaveLoadedDocument; eller kør en one-shot operation såsom EncryptFile, der læser et input og skriver et output. Uanset hvad læser gendannelsen (recovery) /XRefStm, fletter (merges) strøm-sektionen før de klassiske poster og løser (resolves) de objekter, der lever i strømme, før skrivningen (the write) opregner dem. AES-256 krypteringsstien er der, hvor problemet først viste sig, fordi kryptering af et dokument genskriver hvert objekt og dermed kræver, at hvert objekt allerede er blevet lokaliseret
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Den detalje, der er værd at tage med, sidder opstrøms (upstream) for API'et. Filer, der ankommer fra Word, Excel, PowerPoint og en lang række "Gem som PDF"-pipelines, er rutinemæssigt hybride, så en loader, du kun træner mod dit eget generator-output, møder måske aldrig en i test. Såsæt (seed) dine fixtures med dokumenter eksporteret fra rigtige Office-applikationer, ikke kun med filer din egen kode producerede
Kontrol af en fil, du mistænker
To inspektioner afgør hurtigt spørgsmålet. Åbn filen i en hex-visning (hex view) og læs bytene efter den sidste startxref; en hybridfil viser en kort klassisk sektion, hvis trailer-ordbog indeholder /XRefStm. Eller sammenlign det objektantal, en fuld parse rapporterer, med det højeste objektnummer, som /Size erklærer i traileren. Et stort hul betyder, at objekter gemmer sig i strømme (streams), som loaderen ikke har åbnet, hvilket er det samme underskud, der senere bliver til en gemme-tids fejl
Halen på en typisk Excel-eksport gør det første tjek konkret. Alt efter det sidste xref-nøgleord er ren ASCII, så signaturen kan læses direkte ud fra en hex-visning (offsets er illustrative, anmærkninger tilføjet)
xref
0 0 % empty classic subsection: no rows at all
trailer
<< /Size 216 % one past the highest object number in use
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % byte offset of the cross-reference stream
>>
startxref
88710 % points at the classic section above
%%EOF
0 0-undersektionen er afsløringen: en klassisk tabel med nul poster eksisterer kun for at bære traileren, og traileren eksisterer hovedsageligt for at sige /XRefStm 87325. En detektor, der stopper ved xref-nøgleordet, har på dette tidspunkt set et indeks over ingenting. Når du hellere vil scripte tjekket end vurdere det med øjet, sidder markøren altid inden for de sidste par kilobytes af filen, så en afgrænset baglæns læsning er nok
// Returns the /XRefStm offset from the file's tail, or -1 if the
// marker is absent (the file is not hybrid, or not a PDF at all)
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; // the trailer lives in the tail
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // bounded backward read: 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; // no hybrid marker in the tail
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // skip whitespace after the key
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;
// Usage: a non-negative result names the byte where the stream starts
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('hybrid-reference file: resave will need the /XRefStm section');
Betragt sonden (the probe) som triage, ikke som en parser: den fortæller dig, hvilke filer i en batch der fortjener opmærksomhed, før et resave-job kører, og intet mere. Hvad en loader derefter skal gøre med det offset, den finder, følge sektionskæden (section chain), flette strøm-posterne forud for de klassiske, respektere fri-post-vagtposter (free-entry sentinels), gennemgås trin for trin i vores ledsageartikel om håndtering af hybrid-reference PDF'er fra Office-applikationer
Skriverens (writer's) side af denne historie, hvordan objektstrømme og komprimerede krydsreferencer overhovedet produceres, dækkes i vores artikel om objektstrømme og trinvise opdateringer. Når den pågældende hybridfil også er meget stor, lader indlæsningsteknikkerne i Direct File API-gennemgangen for store PDF-workflows dig inspicere den uden at læse det hele ind i hukommelsen. Begge parrer naturligt med den gendannelse, der er beskrevet her, som leveres som en del af HotPDF-komponenten til Delphi og C++Builder sammen med API'erne til indlæsning, redigering, kryptering og signering, der dækkes andre steder på denne blog