Teknisk artikel

Håndtering af hybrid-reference PDF'er fra Office-applikationer i Delphi

Eksporter et dokument fra Microsoft Word eller Excel med Gem som PDF, og filen på disken er oftest en hybrid-reference fil. Den bærer sin krydsreference-information to gange: én gang som den klassiske tabel med fast bredde, der afsluttede enhver PDF op til version 1.4, og én gang som en komprimeret krydsreference-strøm (cross-reference stream), som det meste af dokumentet faktisk afhænger af. En enkelt trailer-nøgle, /XRefStm, syr de to visninger sammen, og om et værktøj ser hele dokumentet koger ned til, om det følger den nøgle

Denne artikel ser på hybrid-filer fra forbrugersiden: hvordan bytes i slutningen af filen ser ud, hvordan de to visninger driver fra hinanden under redigering, og hvordan en Delphi-pipeline kan opdage og dirigere hybrid-inputs. Hvordan en loader fletter visningerne (merges the views), og hvorfor rækkefølgen ikke er til forhandling, er emnet for vores HotPDF-artikel om indlæsning af hybrid-reference filer; denne handler om at genkende layoutet i første omgang

Hvorfor Office-eksporter skriver indekset to gange

PDF 1.5 introducerede to funktioner, der ændrede filens form: krydsreference-strømme, som gemmer objektindekset som komprimerede binære data i stedet for en almindelig teksttabel, og objekt-strømme (object streams), som pakker mange små objekter i én Flate-komprimeret container. En writer, der bruger dem, producerer mindre filer, men en PDF 1.4-læser kan ikke åbne resultatet, fordi de strukturer, den er afhængig af (keys on), xref-nøgleordet og trailer-ordbogen, er væk

ISO 32000-1 §7.5.8.4 definerer kompromiset. En hybrid-reference fil skriver begge dele: en klassisk krydsreference-tabel, der adresserer de objekter, en gammel læser skal nå, heriblandt kataloget og sidetræet, og en krydsreference-strøm, der indekserer alt andet. Objekter foldet ind i objekt-strømme er markeret som frie (free) i den klassiske tabel, så en 1.4-læser springer dem over uden at klage; deres rigtige placeringer eksisterer kun i strømmen. Den klassiske trailer bærer derefter en /XRefStm-nøgle, der holder byte-offsettet for den strøm. En gammel fremviser læser aldrig nøglen og renderer filen fra tabel-visningen. En moderne fremviser følger den og ser hele dokumentet. Word og Excel har udsendt præcis dette layout i årevis, hvilket er grunden til, at hybrid-filer ikke er et eksotisk hjørnetilfælde (corner case), men en stor andel af, hvad forretnings-pipelines modtager

Hvordan halen på en hybrid-fil ser ud

Layoutet er nemmest at forstå ud fra bytes. Her er halen på en lille hybrid-fil, offsets forkortet; i en rigtig Office-eksport er /XRefStm-værdien typisk et stort offset nær slutningen af filen. Læserækkefølgen er 'hale-først'-gennemgangen beskrevet i vores oversigt over PDF-filstruktur: find %%EOF, læs startxref, hop til tabellen

% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...

xref                    % classic section: what startxref points at
0 4
0000000000 65535 f      % slot 0: head of the free list, always present
0000000017 00000 n      % object 1: the catalog, visible to any reader
0000000000 65535 f      % object 2: marked free -- lives in an object stream
0000000000 65535 f      % object 3: same; only the stream view locates it
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % byte offset of the cross-reference stream
>>
startxref
7164                    % byte offset of the 'xref' keyword above
%%EOF

To detaljer i dette dump bærer hele mekanismen. For det første peger startxref på den klassiske sektion, med vilje: det er den adresse, en gammel læser skal lande på. Krydsreference-strømmen kan kun nås gennem /XRefStm-nøglen inde i trailer-ordbogen, så en parser, der aldrig kigger efter den nøgle, finder aldrig ud af, at strømmen eksisterer. For det andet er objekterne 2 og 3 løgne af en godartet slags. Den klassiske tabel erklærer dem frie, men de er rigtige objekter, der sidder inde i en komprimeret container; den frie markering er det, der forhindrer en 1.4-læser i at snuble over poster, den ikke kan bruge. En forbruger, der alene stoler på den klassiske visning, konkluderer, at det meste af dette dokument ikke eksisterer

Hvordan de to visninger driver fra hinanden

En hybrid-fil frisk ud af Word er internt konsistent: begge visninger beskriver det samme dokument, hver inden for sit erklærede omfang. Problemerne starter, når filen redigeres af et værktøj, der kun forstår én af visningerne. Overvej et stemplingsværktøj, der tilføjer en klassisk-stil trinvis opdatering (incremental update): nye objekter, en ny xref-sektion, en /Prev-kæde til den forrige sektion, og en ny trailer. Hvis den trailer dropper /XRefStm-nøglen, er strømvisningen forældreløs (orphaned); hvis den kopierer den gamle værdi fremad, beskriver strømvisningen stadig dokumentet, som det var før redigeringen. Uanset hvad er de to indekser nu uenige om, hvad filen indeholder

Den resulterende fil har en karakteristisk fejlsignatur: objekter synlige i én visning mangler eller er forældede (stale) i den anden. En læser, der løser (resolves) gennem strømvisningen, finder pre-edit-versionen af et opdateret objekt, eller slet ingen post for et tilføjet. En læser på tabel-visningen ser redigeringen, men mister overblikket over de komprimerede objekter, som strømmen alene lokaliserer. I praksis kommer dette til overfladen som formularfelter, der overlever i én fremviser og forsvinder i en anden, annotationer en stemplingsgennemgang (stamping pass) ser ud til at have slettet, eller opslag, der lander på det helt forkerte objekt

Det, der gør disse filer dyre at debugge, er, at Adobe Acrobat normalt åbner dem uden at klage: når indekset er uenigt med bytene, genopbygger det i stilhed krydsreference-dataene ved at scanne efter objektheadere, så den, der har produceret den ødelagte fil, ikke ser noget galt. Fejlen kommer til overfladen senere, når filen når en streng forbruger, en preflight-validator, en signeringstjeneste, et arkiverings-indtagelsesjob (archival ingest job), der stoler på den erklærede struktur og rapporterer manglende objekter eller et krydsreference-misforhold (cross-reference mismatch). "Den åbner fint i Acrobat" er sådan, næsten enhver hybrid desynkroniserings-ticket begynder

Påvisning af en hybrid-fil i ren Delphi

Klassificering af inputs kræver ikke et PDF-bibliotek. /XRefStm-nøglen kan kun forekomme inde i en klassisk trailer-ordbog, og den aktive trailer sidder inden for de sidste par kilobytes af filen, fordi specifikationen kræver, at %%EOF vises nær den fysiske ende. At læse et afgrænset hale-vindue og gennemsøge det er nok til triage:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
  Tail := TEncoding.ANSI.GetString(Buf);

  // Find the LAST 'trailer' keyword: with incremental updates,
  // the newest trailer is the one that governs the file
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // no classic trailer: a pure xref-stream file, not hybrid

  // A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

De tre udfald stemmer overens med de tre layouts. En fil med kun klassisk visning har en trailer, men ingen /XRefStm: False. En fil, der udelukkende benytter krydsreference-strømme, har slet ikke noget trailer-nøgleord, dens trailer-nøgler lever i strøm-ordbogen: også False, korrekt, fordi en sådan fil er komprimeret, ikke hybrid. Kun det dobbelt-indekserede layout returnerer True

Til produktionsbrug er to hærdninger (hardenings) de ekstra linjer værd. Parse heltallet efter /XRefStm, søg (seek) til det offset, og bekræft, at et strøm-objekt med /Type /XRef faktisk sidder der; en trunkeret fil kan bære nøglen, mens strømmen er væk, hvilket hører hjemme i en anden spand (bucket) end en sund hybrid. Og behandl vinduesstørrelsen som en parameter: 2 KB dækker almindeligt Office-output, men en usædvanligt stor trailer-ordbog kan skubbe nøgleordet uden for rækkevidde, og at udvide vinduet slår at erklære filen for klassisk ved et uheld

Routing af hybrid-filer gennem en Delphi-pipeline

Påvisning (detection) køber dig en routingbeslutning. For filer, der kun læses, renderes eller valideres, brug en loader, der løser begge visninger, og bekræft (verify) derefter adfærd i stedet for bytes. PDFium-komponenten parser /XRefStm-kæden under indlæsning, så den objekttabel, din kode ser, er den sammenflettede (merged), og tjekkene beskrevet i vores artikel om validering af objekt- og krydsreference-strømme gælder uændret. Hvis en desynkroniseret hybrid er beskadiget alvorligt nok til at afvise indlæsning, rapporterer motoren det gennem sit fejlsæt, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY og FPDF_ERR_PAGE, med FPDF_ERR_FORMAT som den én strukturel skade producerer. Læn dig dog ikke op ad det signal: PDFium er lempelig af design og genopbygger de fleste inkonsekvente filer lydløst, så en vellykket indlæsning beviser, at filen kunne gendannes, ikke at dens to visninger er enige. Det meningsfulde konsistenstjek er at sammenligne, hvad en fuld objekt-gennemgang (object walk) finder, mod hvad trailerens /Size erklærer

For filer, din pipeline ændrer, er den sikreste politik at stoppe dem fra at være hybride overhovedet. En indlæsning efterfulgt af en fuld gemning gennem HotPDF omskriver dokumentet med en enkelt, selv-konsistent krydsreference i én form: ingen /XRefStm, ingen anden visning at falde ud af synkronisering, hvert objekt ejet af nøjagtigt én indekspost. Den normalisering er det, du vil have før arkiverings-indtagelse (archival ingest), før en streng downstream RIP eller signeringstjeneste, og efter enhver redigering anvendt på et hybrid-input. Det fungerer, fordi loaderen flettede visningerne korrekt på vej ind, den mekanisme som HotPDF hybrid-reference artiklen gennemgår i detaljer

Den ene klasse af filer, man skal lade være, er digitalt signerede dokumenter. En fuld omskrivning flytter hver byte, hvilket ugyldiggør enhver signatur beregnet over de originale områder. En ændring af en signeret hybrid skal gå ind som en ordentlig trinvis opdatering (incremental update), der opretholder begge visninger; en fil, der kun skal læses, skal passere uberørt. Normalisering er for filer, du ejer; signerede filer tilføjer (append) du kun nogensinde til

Hybrid-reference PDF'er er ikke forkert udformede (malformed); de er formatets egen kompatibilitetsbro, og Office-applikationer vil blive ved med at producere dem, så længe PDF 1.4-læsere overlever i installationsbasen. En pipeline, der kan spotte /XRefStm-nøglen, validere det flettede dokument med PDFium-komponenten og regenerere rent enkelt-indeks output med HotPDF-komponenten, behandler dem som det de er: almindelige inputs med et ekstra skilt i traileren