Technical Article

Indlæsning af hybrid-reference-PDF'er fra Word og Excel i Delphi

Åbn en PDF, som Microsoft Word eller Excel har produceret, bladr igennem den, og intet ser usædvanligt ud. Indlæs den i et Delphi-program, læs sideantallet, og tallet er korrekt. Gem den derefter igen med kryptering slået til, og opgaven fejler med en EListError, eller outputtet åbnes med en advarsel om beskadiget krydsreference. Filen var aldrig beskadiget. Det er en hybrid-referencefil, og selve den struktur, der lader en femten år gammel fremviser åbne den, er den struktur, der overvinder en indlæser, som stopper med at læse for tidligt.

Dette er en af de mest almindelige måder, hvorpå en PDF-pipeline, der bestod alle interne test, møder en fil, som den ikke kan behandle fuldstændigt. Inputtene var alle genereret internt, så de var aldrig hybride. Den første hybride fil 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, mens den still lader en PDF 1.4-læser åbne filen, skriver krydsreference-oplysningerne to gange. Der er en klassisk krydsreferencetabel, de ASCII-rækker med fast bredde, der afsluttede enhver PDF op til version 1.4, og der er en krydsreferencestrøm, der indekserer resten. Traileren til den klassiske sektion indeholder en /XRefStm-indgang, hvis værdi er byte-forskydningen af denne 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 ledige i den klassiske tabel med en indgang af typen f, så en 1.4-læser springer direkte forbi dem og aldrig snubler over en struktur, den ikke kan tolke. Deres reelle placeringer findes kun i krydsreferencestrømmen. Signaturen på en sådan fil er dens hale: en kort klassisk sektion, ofte intet andet end xref efterfulgt af en 0 0 undersektionsoverskrift, hvis trailer peger på /XRefStm, hvor de faktiske gendannelsesdata befinder sig.

Hvorfor et korrekt sideantal intet beviser

Fordi kataloget og sidetræet med vilje er tilgængelige fra den klassiske tabel, finder en indlæser, der kun læser denne 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, taggede PDF-strukturelementer, den lange hale af små ordbøger, der aldrig behøvede at være synlige for en ældre fremviser.

Du bemærker ikke hullet, før noget rører ved disse objekter, og en fuld gengemning rører ved dem alle. At gennemgå dokumentet for at genkryptere eller genskrive det er præcis den handling, der anmoder om hvert objektnummer efter tur, hvilket er grunden til, at symptomet dukker op ved gemmetid i stedet for indlæsningstid, langt fra sin årsag.

Fælden er en detektor, der ser xref og stopper

Den nemme måde at afgøre, hvordan en fil er indekseret på, er at følge startxref og inspicere de første bytes, den peger på. Nøgleordet xref betyder en klassisk tabel; et strømobjekt betyder en krydsreferencestrøm. Denne test er korrekt for enhver fil, der forpligter sig til ét system. Den er forkert for en hybridfil, hvis startxref peger på en klassisk sektion med det eneste formål at tilfredsstille gamle læsere, mens /XRefStm i denne 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 detektoren for tidlig afslutning på plads ser indlæsningen fin ud, og gengemningen er der, hvor de manglende objekter melder sig. Løsningen er ikke at læse flere bytes i starten; det handler om at genkende hybrid-traileren og følge /XRefStm, før det besluttes, at filen er færdig.

Fletningsrækkefølgen er ikke til forhandling

Når begge indekserer læst, kan de kun kombineres i én retning. Krydsreferencestrømmen skal flettes først, med de klassiske indgange udfyldt omkring den. Årsagen er det lille bedrag i hjertet af formatet. En hybridfil markerer sine komprimerede objekter som ledige i den klassiske tabel, så gamle læsere ignorerer dem. En indlæser, der respekterer en først-set-vinder-politik og læser den klassiske tabel først, vil registrere disse objektnumre som ledige og derefter kassere de strømindgange, der faktisk placerer dem, fordi pladserne allerede er optaget. Vend rækkefølgen om, og type 2-indgangene fra strømmen, der hver især er et objektstrømnummer plus et indeks, vinder de pladser, de skal eje, og de klassiske indgange lægger sig omkring dem.

Den samme disciplin beskytter mod, at en ældre revision genopliver et slettet objekt. Trinvise opdateringer kæder bagud gennem /Prev, og en type 0 frit-indgang er en sentinel, der viser, at en nyere sektion har pensioneret et objektnummer. En senere, ældre sektion i kæden må ikke få lov til at overskrive denne sentinel med en forældet placering. Behandl først-set som autoritativt for ledige markører, og det slettede objekt forbliver slettet; behandl det skødesløst, og filens egen historik genopliver indhold, som den seneste revision fjernede.

Hvad dette betyder i HotPDF

Motoren løser hybrid-referencefiler for dig, og den gør det på enhver sti, der skal fortolke krydsreferencedata. Indlæs et dokument med LoadFromFile eller LoadFromStream, foretag dine ændringer, og kald SaveLoadedDocument; eller kør en enkeltstående handling som f.eks. EncryptFile, der læser et input og skriver et output. Uanset hvad læser gendannelsen /XRefStm, fletter strømsektionen forud for de klassiske indgange og løser de objekter, der lever i strømme, før skrivningen 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 placeret.

// 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 huske, ligger opstrøms for API'en. Filer, der ankommer fra Word, Excel, PowerPoint og en lang række "Gem som PDF"-pipelines, er rutinemæssigt hybride, så en indlæser, du kun tester mod dit eget generator-output, møder måske aldrig en under test. Fyld dine testmiljøer med dokumenter eksporteret fra rigtige Office-applikationer, ikke kun med filer, dit eget kode har produceret.

Kontrol af en fil, du har mistanke til

To inspektioner afgør hurtigt spørgsmålet. Åbn filen i en hex-visning og læs bytes efter den afsluttende startxref; en hybridfil viser en kort klassisk sektion, hvis trailer-ordbog indeholder /XRefStm. Or sammenlign det objektantal, en fuld fortolkning rapporterer, med det højeste objektnummer, som /Size erklærer i traileren. Et stort gab betyder, at objekter gemmer sig i strømme, som indlæseren ikke har åbnet, hvilket er den samme mangel, som bliver til en fejl ved gemmetid senere.

Skriverens side af denne historie, hvordan objektstrømme og komprimerede krydsreferencer produceres i første omgang, er dækket i vores artikel om objektstrømme og trinvise opdateringer. Når den pågældende hybridfil også er meget stor, tillader indlæsningsteknikkerne i gennemgangen af Direct File API til store PDF-arbejdsgange dig at inspicere den uden at læse det hele ind i hukommelsen. Begge parres naturligt med den her beskrevne genopretning, som leveres som en del af HotPDF Component til Delphi og C++Builder sammen med API'erne til indlæsning, redigering, kryptering og signering, der er dækket andre steder på denne blog.