Technisch artikel

Omgaan met Hybride-Referentie PDF's van Office Applicaties in Delphi

Exporteer een document vanuit Microsoft Word of Excel met "Opslaan als PDF" en het bestand op schijf is in de meeste gevallen een hybride-referentie bestand (hybrid-reference file). Het bevat (carries) zijn cross-reference informatie dubbel: eenmaal als de klassieke vaste-breedte (fixed-width) tabel die elke PDF tot aan versie 1.4 afsloot, en eenmaal als een gecomprimeerde cross-reference stream waarvan het grootste deel van het document daadwerkelijk afhankelijk is. Eén enkele trailer-sleutel (trailer key), /XRefStm, hecht de twee weergaven samen, en of een tool het gehele document ziet hangt ervan af of deze die sleutel volgt (follows)

Dit artikel bekijkt hybride bestanden vanuit de consumerende (consuming) kant: hoe de bytes aan het einde van het bestand eruitzien, hoe de twee weergaven uiteendrijven (drift apart) tijdens het bewerken, en hoe een Delphi-pijplijn (pipeline) hybride invoer kan detecteren en routeren. Hoe een lader (loader) de weergaven samenvoegt, en waarom de volgorde niet onderhandelbaar (negotiable) is, is het onderwerp van ons HotPDF-artikel over het laden van hybride-referentie bestanden; dit artikel gaat over het in eerste instantie (in the first place) herkennen van de layout

Waarom Office-exports de index tweemaal schrijven

PDF 1.5 introduceerde twee functies die de vorm (shape) van het bestand veranderden: cross-reference streams, die de objectindex opslaan als gecomprimeerde binaire data in plaats van een plaintext tabel, en object streams, die vele kleine objecten samenpakken in één Flate-gecomprimeerde container. Een schrijver (writer) die ze gebruikt produceert kleinere bestanden, maar een PDF 1.4 lezer (reader) kan het resultaat niet openen, omdat de structuren waarop hij sleutelt (keys on), het xref trefwoord en de trailer woordenboek (dictionary), verdwenen zijn

ISO 32000-1 §7.5.8.4 definieert het compromis. Een hybride-referentie bestand schrijft beide: een klassieke cross-reference tabel die de objecten adresseert die een oude lezer moet kunnen bereiken, waaronder de catalogus en de paginaboom (page tree), en een cross-reference stream die al de rest indexeert. Objecten die zijn samengevouwen (folded into) in object streams worden als vrij (free) gemarkeerd in de klassieke tabel, dus een 1.4 lezer slaat ze zonder klagen over; hun echte locaties bestaan alleen in de stream. De klassieke trailer draagt dan een /XRefStm sleutel (key) die de byte-offset van die stream bevat. Een oude viewer leest de sleutel nooit en rendert het bestand vanuit de tabelweergave. Een moderne viewer volgt deze (follows it) en ziet het complete document. Word en Excel stoten deze lay-out al jaren precies zo uit, en daarom zijn hybride bestanden geen exotisch randgeval (corner case) maar een groot deel van wat zakelijke (business) pijplijnen ontvangen

Hoe de staart (tail) van een hybride bestand eruitziet

De lay-out is het makkelijkst te begrijpen vanuit de bytes. Hier is de staart van een klein hybride bestand, waarbij offsets zijn ingekort; in een echte Office-export is de /XRefStm waarde typisch een grote offset nabij het einde van het bestand. De leesvolgorde is de achterstevoren-wandeling (tail-first walk) beschreven in ons overzicht van de PDF-bestandsstructuur: zoek %%EOF, lees startxref, spring naar de tabel

% ... 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

Twee details in deze dump dragen (carry) het hele mechanisme. Ten eerste, startxref wijst naar de klassieke sectie, met opzet (on purpose): dat is het adres (address) waarop een oude lezer moet landen. De cross-reference stream is alleen bereikbaar via de /XRefStm sleutel in het trailer woordenboek (dictionary), dus een parser die nooit naar die sleutel zoekt, leert nooit dat de stream bestaat. Ten tweede, de objecten 2 en 3 zijn leugens (lies) van een goedaardige soort (benign kind). De klassieke tabel verklaart ze vrij, maar het zijn echte objecten die in een gecomprimeerde container zitten; de vrij-markering (free marking) weerhoudt (keeps) een 1.4 lezer ervan om te struikelen over items die hij niet kan gebruiken. Een consument die alleen de klassieke weergave vertrouwt, concludeert dat het grootste deel van dit document niet bestaat

Hoe de twee weergaven uiteendrijven (drift apart)

Een hybride bestand vers (fresh out) uit Word is intern consistent: beide weergaven beschrijven hetzelfde document, elk binnen zijn gedeclareerde (declared) scope. De problemen beginnen wanneer het bestand wordt bewerkt door een tool die slechts één van de weergaven begrijpt. Overweeg (Consider) een stempel-programma (stamping utility) dat een incrementele update in klassieke stijl toevoegt: nieuwe objecten, een nieuwe xref sectie, een /Prev keten naar de vorige sectie, en een nieuwe trailer. Als die trailer de /XRefStm sleutel laat vallen (drops), is de stream-weergave verweesd (orphaned); als hij de oude waarde vooruit kopieert (copies forward), beschrijft de stream-weergave nog steeds het document zoals het vóór de bewerking (edit) was. Hoe dan ook, de twee indexen (indexes) zijn het nu oneens (disagree) over wat het bestand bevat

Het resulterende bestand heeft een kenmerkende (distinctive) fout-signatuur (failure signature): objecten die zichtbaar zijn in de ene weergave missen of zijn verouderd (stale) in de andere. Een lezer die oplost (resolves) via de stream-weergave vindt de voor-bewerking (pre-edit) versie van een geüpdatet object, of helemaal geen (no entry at all) voor een toegevoegd object. Een lezer op basis van de tabel-weergave (table view) ziet de bewerking (edit), maar raakt het spoor bijster (loses track) van de gecomprimeerde objecten die uitsluitend door de stream worden gelokaliseerd (locates). In de praktijk duikt dit op (surfaces) als formuliervelden die de ene viewer overleven (survive) en in een andere verdwijnen, als annotaties die een stempel-doorgang (stamping pass) schijnbaar (appears to have) heeft verwijderd, of als zoekopdrachten (lookups) die op een compleet verkeerd (wrong object entirely) object landen

Wat deze bestanden duur maakt om te debuggen is dat Adobe Acrobat ze gewoonlijk (usually) zonder klagen opent: wanneer de index het niet eens is met de bytes, bouwt hij in stilte (quietly rebuilds) de cross-reference data opnieuw op door te scannen naar object-headers (headers), dus wie het kapotte bestand ook produceerde (produced) ziet niets verkeerd. De storing (failure) komt pas later aan de oppervlakte, wanneer het bestand een strikte (strict) consument (consumer), een preflight-validator, een ondertekeningsdienst (signing service) of een archiefopnametaak (archival ingest job) bereikt, die de gedeclareerde structuur vertrouwt en ontbrekende objecten of een cross-reference mismatch rapporteert. "Het opent prima in Acrobat" is hoe zowat elk ticket (ticket) over hybride desynchronisatie begint

Een hybride bestand detecteren in pure Delphi

Het classificeren (Classifying) van inputs vereist (does not require) geen PDF-bibliotheek. De /XRefStm sleutel (key) kan alleen voorkomen binnen een klassiek trailer-woordenboek (dictionary), en de actieve trailer bevindt zich (sits) binnen de laatste paar kilobytes van het bestand, omdat de specificatie (specification) vereist dat %%EOF nabij (near) het fysieke einde verschijnt. Het lezen (Reading) van een begrensde (bounded) staart-venster (tail window) en deze doorzoeken is voldoende (enough) voor 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 drie uitkomsten komen overeen met de drie lay-outs. Een exclusief klassiek (classic-only) bestand heeft een trailer maar geen /XRefStm: Onwaar (False). Een bestand dat zich volledig overgeeft (commits fully) aan cross-reference streams heeft helemaal geen (no) trailer trefwoord (keyword), zijn trailer-sleutels (keys) wonen (live) in het stream-woordenboek: ook Onwaar, correct, omdat zo'n bestand gecomprimeerd (compressed) is, niet hybride. Alleen de dubbel-geïndexeerde lay-out retourneert Waar (True)

Voor productiegebruik zijn twee verhardingen (hardenings) de extra regels (extra lines) waard. Ontleed (Parse) het gehele getal (integer) na /XRefStm, zoek naar (seek to) die offset, en bevestig dat daar werkelijk (actually) een stream-object (stream object) met /Type /XRef staat; een afgebroken (truncated) bestand kan de sleutel bevatten (carry) terwijl de stream verdwenen is, en zoiets hoort in een andere emmer (bucket) dan een gezonde hybride (healthy hybrid). En behandel de venstergrootte (window size) als parameter: 2 KB dekt de gangbare Office-uitvoer, maar een ongewoon groot trailer-woordenboek kan het trefwoord buiten bereik (out of range) duwen, en het verbreden van het venster is beter (beats) dan per ongeluk (by accident) verklaren (declaring) dat het bestand klassiek (classic) is

Hybride bestanden routeren door een Delphi-pijplijn

Detectie levert (buys) u een routeringsbeslissing (routing decision) op. Voor bestanden die enkel worden gelezen, gerenderd of gevalideerd, gebruikt u een lader (loader) die beide weergaven (views) oplost (resolves), en verifieert (verify) u vervolgens het gedrag (behavior) in plaats van de bytes. De PDFium Component ontleedt (parses) de /XRefStm-ketting tijdens het laden, dus de objecttabel die uw code ziet is de samengevoegde, en de controles (checks) beschreven in ons artikel over het valideren van object- en cross-reference streams zijn ongewijzigd van toepassing. Als een gedesynchroniseerde hybride zodanig beschadigd is dat laden (loading) wordt geweigerd, meldt de engine dit via de reeks (set) foutmeldingen: FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY en FPDF_ERR_PAGE, waarbij FPDF_ERR_FORMAT degene is die door structurele schade (structural damage) wordt gegenereerd. Leun echter niet te zwaar (lean) op dat signaal: PDFium is uit principe (by design) soepel (lenient) en herstelt de meeste inconsistente bestanden zonder morren (silently), waardoor succesvol laden slechts bewijst dat het bestand te redden was, niet dat de twee weergaven met elkaar in overeenstemming (agree) zijn. De betekenisvolle (meaningful) controle van de consistentie (consistency check) is het vergelijken (comparing) van wat een volledige inspectie (walk) van de objecten aantreft (finds) met wat /Size van de trailer verklaart (declares)

Voor bestanden die door uw pijplijn worden aangepast (modifies), is het de veiligste (safest) strategie (policy) om ze helemaal (at all) niet meer hybride te laten zijn. Laden (load) gevolgd door volledig opslaan via HotPDF overschrijft (rewrites) het document met een enkelvoudige (single), zelf-consistente cross-reference in één enkele vorm: geen /XRefStm, geen tweede weergave die uit de pas kan gaan lopen (to fall out of sync), en waarbij elk object door exact (exactly) één item in de index (index entry) toebehoord (owned). Die normalisatie is wat u wilt vóór een archiveringsopname (archival ingest), vóór een strenge RIP (raster image processor) verder in de keten of een ondertekeningsdienst (signing service), en ná elke aanpassing die op hybride invoer wordt toegepast (applied). Dit werkt omdat de lader de weergaven tijdens de invoer correct (correctly) heeft gebundeld (merged), een mechanisme dat in het HotPDF artikel over hybride referenties (hybrid-reference) uitvoerig (in detail) wordt behandeld (walks through)

De enige klasse van bestanden die ongemoeid moet worden gelaten (to leave alone), zijn digitaal ondertekende (signed) documenten. Een volledige herschrijving verplaatst (moves) elke byte, waardoor elke handtekening berekend over de oorspronkelijke reeksen (ranges) ongeldig wordt (invalidates). Een wijziging (change) van een ondertekende hybride (signed hybrid) moet er in als een keurige (proper) incrementele update (incremental update) die beide weergaven handhaaft (maintains); een bestand dat enkel gelezen hoeft te worden, zou onaangeroerd (untouched) moeten doorstromen (pass through). Normalisatie (Normalization) is voor bestanden waarvan u de eigenaar bent (you own); ondertekende bestanden waar u alleen ooit data aan toevoegt (append to)

Hybride-referentie PDF's zijn niet misvormd (malformed); ze vormen de eigen compatibiliteitsbrug (compatibility bridge) van het formaat, en Office applicaties zullen ze blijven (keep) produceren (producing) zolang PDF 1.4-lezers voortbestaan (survive) in het installatiebestand. Een pijplijn die de /XRefStm-sleutel kan herkennen (spot), het samengevoegde (merged) document kan valideren met de PDFium Component, en loepzuivere (clean) output met enkelvoudige index (single-index output) opnieuw kan genereren (regenerate) met de HotPDF Component, beschouwt ze voor wat ze zijn: gewone (ordinary) invoer met één extra wegwijzer in de trailer