Teknisk artikel

Hantera PDF-filer med hybridreferens från Office-program i Delphi

Exportera ett dokument från Microsoft Word eller Excel med Spara som PDF och filen på disken är oftast en hybridreferensfil. Den bär sin korsreferensinformation två gånger: en gång som den klassiska tabellen med fast bredd som avslutade varje PDF upp till version 1.4, och en gång som en komprimerad korsreferensström som större delen av dokumentet faktiskt är beroende av. En enda trailernyckel, /XRefStm, syr ihop de två vyerna, och om ett verktyg ser hela dokumentet kokar ner till om det följer den nyckeln

Den här artikeln tittar på hybridfiler från konsumentsidan: hur byten i slutet av filen ser ut, hur de två vyerna glider isär under redigering, och hur en Delphi-pipeline kan upptäcka och dirigera hybridinmatningar. Hur en inläsare slår ihop vyerna, och varför ordningen inte är förhandlingsbar, är ämnet för vår HotPDF-artikel om inläsning av hybridreferensfiler; den här handlar om att känna igen layouten i första hand

Varför Office-exporter skriver indexet två gånger

PDF 1.5 introducerade två funktioner som ändrade filens form: korsreferensströmmar (cross-reference streams), som lagrar objektindexet som komprimerad binär data i stället för en klartext-tabell, och objektströmmar, som packar många små objekt i en Flate-komprimerad behållare. En skrivare som använder dem producerar mindre filer, men en PDF 1.4-läsare kan inte öppna resultatet, eftersom de strukturer den bygger på, nyckelordet xref och trailer-ordboken, är borta

ISO 32000-1 §7.5.8.4 definierar kompromissen. En hybridreferensfil skriver båda: en klassisk korsreferenstabell som adresserar de objekt en äldre läsare måste nå, katalogen och sidträdet bland dem, och en korsreferensström som indexerar allt annat. Objekt som är vikta in i objektströmmar markeras som lediga (free) i den klassiska tabellen, så en 1.4-läsare hoppar över dem utan att klaga; deras riktiga platser finns bara i strömmen. Den klassiska trailern bär sedan en /XRefStm-nyckel som innehåller byte-förskjutningen för den strömmen. En gammal visare läser aldrig nyckeln och renderar filen från tabellvyn. En modern visare följer den och ser hela dokumentet. Word och Excel har avgett exakt den här layouten i flera år, vilket är anledningen till att hybridfiler inte är ett exotiskt hörnfall utan en stor del av vad företagspipelines tar emot

Hur svansen på en hybridfil ser ut

Layouten är lättast att förstå från bytes. Här är svansen på en liten hybridfil, med förkortade förskjutningar; i en riktig Office-export är /XRefStm-värdet normalt en stor förskjutning nära slutet av filen. Läsordningen är den svans-först-genomgång (tail-first walk) som beskrivs i vår översikt av PDF-filstruktur: hitta %%EOF, läs startxref, hoppa till 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

Två detaljer i denna dump bär hela mekanismen. För det första pekar startxref med flit på den klassiska sektionen: det är adressen som en äldre läsare måste landa på. Korsreferensströmmen är endast nåbar via /XRefStm-nyckeln inuti trailer-ordboken, så en tolkare som aldrig letar efter den nyckeln får aldrig veta att strömmen existerar. För det andra är objekt 2 och 3 lögner av det godartade slaget. Den klassiska tabellen förklarar dem lediga, men de är verkliga objekt som sitter i en komprimerad behållare; den lediga markeringen är det som hindrar en 1.4-läsare från att snubbla över poster den inte kan använda. En konsument som bara litar på den klassiska vyn drar slutsatsen att merparten av det här dokumentet inte existerar

Hur de två vyerna glider isär

En hybridfil färsk från Word är internt konsekvent: båda vyerna beskriver samma dokument, var och en inom dess deklarerade räckvidd. Problemet börjar när filen redigeras av ett verktyg som bara förstår en av vyerna. Överväg ett stämplingsverktyg som lägger till en stegvis uppdatering (incremental update) i klassisk stil: nya objekt, en ny xref-sektion, en /Prev-kedja till den föregående sektionen, och en ny trailer. Om den trailern släpper /XRefStm-nyckeln blir strömvyn föräldralös; om den kopierar det gamla värdet framåt beskriver strömvyn fortfarande dokumentet som det var före redigeringen. Oavsett vilket så är de två indexen nu oense om vad filen innehåller

Den resulterande filen har en distinkt felsignatur: objekt som är synliga i den ena vyn saknas eller är inaktuella i den andra. En läsare som löser upp via strömvyn hittar den icke-redigerade versionen av ett uppdaterat objekt, eller ingen post alls för ett tillagt objekt. En läsare på tabellvyn ser redigeringen men tappar spåret av de komprimerade objekten som strömmen ensam lokaliserar. I praktiken visar sig detta som formulärfält som överlever i en visare och försvinner i en annan, kommentarer som en stämplingsomgång verkar ha raderat, eller uppslag som landar på helt fel objekt

Det som gör dessa filer dyra att felsöka är att Adobe Acrobat vanligtvis öppnar dem utan att klaga: när indexet inte stämmer överens med byten bygger det i tysthet om korsreferensdatan genom att skanna efter objekthuvuden, så den som producerade den trasiga filen ser inget fel. Felet dyker upp senare, när filen når en strikt konsument, en preflight-validerare, en signeringstjänst eller ett arkivintags-jobb som litar på den deklarerade strukturen och rapporterar saknade objekt eller en felmatchning av korsreferenser. "Den öppnas bra i Acrobat" är hur nästan varje ärende om hybriddesynkronisering börjar

Att upptäcka en hybridfil i ren Delphi

Att klassificera inmatningar kräver inte ett PDF-bibliotek. Nyckeln /XRefStm kan bara förekomma inuti en klassisk trailer-ordbok, och den aktiva trailern sitter inom de sista par kilobyten av filen, eftersom specifikationen kräver att %%EOF ska visas nära det fysiska slutet. Att läsa ett avgränsat svansfönster och genomsöka det räcker för 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 utfallen ligger i linje med de tre layouterna. En klassisk-endast-fil har en trailer men ingen /XRefStm: False. En fil som fullt ut åtar sig korsreferensströmmar har inget trailer-nyckelord alls, dess trailernycklar lever i strömordboken: också False, korrekt nog, eftersom en sådan fil är komprimerad, inte hybrid. Endast layouten med dubbelt index returnerar True

För produktionsanvändning är två härdningar värda de extra raderna. Tolka heltalet efter /XRefStm, sök till den förskjutningen och bekräfta att ett strömobjekt med /Type /XRef faktiskt sitter där; en avklippt (truncated) fil kan bära nyckeln medan strömmen är borta, vilket hör hemma i en annan kategori än en frisk hybrid. Och behandla fönsterstorleken som en parameter: 2 KB täcker vanliga Office-utdata, men en ovanligt stor trailer-ordbok kan skjuta nyckelordet utom räckhåll, och att vidga fönstret slår att deklarera filen klassisk av en händelse

Att dirigera hybridfiler genom en Delphi-pipeline

Upptäckten ger dig ett dirigeringsbeslut. För filer som bara läses, renderas eller valideras, använd en inläsare som löser upp båda vyerna, och verifiera sedan beteende snarare än bytes. PDFium Component tolkar /XRefStm-kedjan vid inläsning, så objekt-tabellen din kod ser är den sammanslagna, och kontrollerna som beskrivs i vår artikel om validering av objekt och korsreferensströmmar gäller oförändrade. Om en desynkroniserad hybrid är skadad tillräckligt allvarligt för att vägra inläsning rapporterar motorn det genom dess feluppsättning (error set), FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY och FPDF_ERR_PAGE, med FPDF_ERR_FORMAT den som strukturell skada producerar. Förlita dig dock inte på den signalen: PDFium är medvetet tolerant och bygger i tysthet om de flesta inkonsekventa filer, så en lyckad inläsning bevisar att filen gick att rädda, inte att dess två vyer stämmer överens. Den meningsfulla konsekvenskontrollen är att jämföra vad en fullständig objektgenomgång hittar med vad trailerns /Size deklarerar

För filer som din pipeline ändrar är den säkraste policyn att hindra dem från att vara hybrid överhuvudtaget. En inläsning följt av en fullständig sparning genom HotPDF skriver om dokumentet med en enda, självkonsistent korsreferens i ett formulär: ingen /XRefStm, ingen andra vy som faller ur synk, varje objekt ägs av exakt en indexpost. Den normaliseringen är vad du vill ha före ett arkivintag, före en strikt RIP eller signeringstjänst i ett senare led, och efter all redigering som tillämpas på en hybridinmatning. Det fungerar eftersom inläsaren sammanfogade vyerna korrekt på vägen in, den mekanism som HotPDF:s artikel om hybridreferens går igenom i detalj

Den enda klassen av filer att lämna ifred är digitalt signerade dokument. En fullständig omskrivning flyttar varje byte, vilket ogiltigförklarar all signatur som beräknats över de ursprungliga intervallen. En ändring av en signerad hybrid måste gå in som en ordentlig stegvis uppdatering (incremental update) som upprätthåller båda vyerna; en fil som bara behöver läsas bör passera orörd. Normalisering är för filer du äger; signerade filer lägger du bara till i (append)

Hybridreferens-PDF:er är inte felformaterade; de är formatets egen kompatibilitetsbrygga, och Office-program kommer att fortsätta producera dem så länge PDF 1.4-läsare överlever i installationsbasen. En pipeline som kan upptäcka /XRefStm-nyckeln, validera det sammanslagna dokumentet med PDFium Component, och återskapa rena utdata med ett enda index med HotPDF Component behandlar dem som vad de är: ordinära inmatningar med en extra vägskylt i trailern