Teknisk artikkel

Laste hybrid-referanse PDF-er fra Word og Excel i Delphi

Å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