Teknisk artikel

Ladda hybridreferens-PDF-filer från Word och Excel i Delphi

Öppna en PDF som Microsoft Word eller Excel har producerat, bläddra igenom den, och inget ser ovanligt ut. Ladda in den i ett Delphi-program, läs av sidantalet, och siffran är rätt. Spara den sedan igen med kryptering påslagen och jobbet misslyckas med ett EListError, eller så öppnas utdatafilen med en varning om skadad korsreferens. Filen var aldrig korrupt. Det är en hybridreferensfil, och just den struktur som låter en femton år gammal visare öppna den är strukturen som besegrar en laddare som slutar läsa för tidigt

Detta är ett av de vanligaste sätten en PDF-pipeline som klarat varje internt test stöter på en fil den inte kan spara om (round-trip). Indatafilerna genererades alla internt, så de var aldrig hybrider. Den första hybridfilen anländer den dag en kund vidarebefordrar en faktura exporterad från ett kalkylblad

Vad Word och Excel faktiskt skriver

ISO 32000-1 beskriver hybridreferenslayouten i §7.5.8.4. En applikation som vill ha PDF 1.5-funktioner som objektströmmar (object streams), samtidigt som den låter en PDF 1.4-läsare öppna filen, skriver korsreferensinformationen två gånger. Det finns en klassisk korsreferenstabell, ASCII-raderna med fast bredd som avslutade varje PDF upp till version 1.4, och det finns en korsreferensström som indexerar resten. Trailern för den klassiska sektionen bär en /XRefStm-post vars värde är byteoffseten för den strömmen

Arbetsdelningen är avsiktlig. Objekt som en gammal läsare måste nå, katalogen och sidträdet bland dem, är adresserbara från den klassiska tabellen. Objekt som veks in i komprimerade objektströmmar markeras som lediga i den klassiska tabellen, med en post av typen f, så en 1.4-läsare hoppar rakt förbi dem och snubblar aldrig över en struktur den inte kan tolka. Deras verkliga platser lever bara i korsreferensströmmen. Signaturen för en sådan fil är dess slutdel (tail): en kort klassisk sektion, ofta inget mer än xref följt av en 0 0-undersektionsrubrik, vars trailer pekar på det /XRefStm där de faktiska återställningsdata sitter

Varför ett korrekt sidantal inte bevisar någonting

Eftersom katalogen och sidträdet är nåbara från den klassiska tabellen med flit, hittar en laddare som bara läser den tabellen /Root, vandrar igenom sidträdet och rapporterar rätt antal sidor. Allt en gammal läsare behöver finns där, så filen verkar frisk. Objekten som saknades är de som packades in i objektströmmar: AcroForm-fältdiskussioner, strukturelement för taggade PDF:er, den långa raden av små ordböcker (dictionaries) som aldrig behövde vara synliga för en äldre visare

Du märker inte gapet förrän något rör de objekten, och att spara om i sin helhet rör alla av dem. Att gå igenom dokumentet för att kryptera om det eller skriva om det är precis den operation som ber om varje objektnummer i tur och ordning, vilket är anledningen till att symptomet dyker upp vid sparning snarare än inläsning, långt från dess orsak

Fällan är en detektor som ser xref och stannar

Det billiga sättet att avgöra hur en fil är indexerad är att följa startxref och inspektera de första byten det pekar på. Nyckelordet xref betyder en klassisk tabell; ett strömobjekt betyder en korsreferensström. Det testet stämmer för varje fil som förbinder sig till ett schema. Det är fel för en hybridfil, vars startxref siktar på en klassisk sektion av den enda anledningen att tillfredsställa gamla läsare, medan /XRefStm i den sektionens trailer är där större delen av dokumentet faktiskt indexeras. En detektor som returnerar "klassisk" på det första xref den möter läser aldrig /XRefStm, och varje objekt som bara lever i strömmen blir osynligt

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count är korrekt
    // inspektera eller redigera det inlästa dokumentet här
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // går igenom varje objekt
  finally
    Pdf.Free;
  end;
end;

Med tidig-exit-detektorn på plats ser inläsningen bra ut och omsparningen är där de frånvarande objekten tillkännager sig. Fixen är inte att läsa fler bytes i början; det är att känna igen hybridtrailern och följa /XRefStm innan man beslutar att filen är klar

Sammanslagningsordning är inte förhandlingsbar

När båda indexen har lästs in kan de bara kombineras i en riktning. Korsreferensströmmen måste slås ihop först, med de klassiska posterna ifyllda runt omkring den. Anledningen är det lilla bedrägeriet i hjärtat av formatet. En hybridfil markerar sina komprimerade objekt som lediga i den klassiska tabellen så att gamla läsare ignorerar dem. En laddare som respekterar en först-sedda-vinner-policy och läser den klassiska tabellen först kommer att registrera de objektnumren som lediga, och sedan kasta bort de strömposter som faktiskt lokaliserar dem, eftersom platserna redan är upptagna. Reversera ordningen och posterna av typ 2 från strömmen, var och en ett objektströmnummer plus ett index, vinner de platser de är menade att äga, och de klassiska posterna sätter sig runt dem

Samma disciplin skyddar mot att en äldre revision återupplivar ett borttaget objekt. Inkrementella uppdateringar länkar bakåt via /Prev, och en ledig post av typ 0 är en vaktpost som anger att en nyare sektion har pensionerat ett objektnummer. En senare, äldre sektion i kedjan får inte tillåtas att skriva över den vaktposten med en gammal (stale) plats. Behandla först-sedda som auktoritativ för ledig-markörer och det borttagna objektet förblir borttaget; behandla den vårdslöst och en fils egen historia återupplivar innehåll som den senaste revisionen tog bort

Vad detta betyder i HotPDF

Motorn löser hybridreferensfiler åt dig, och den gör det på varje bana som måste tolka korsreferensdata. Ladda in ett dokument med LoadFromFile eller LoadFromStream, gör dina ändringar och anropa SaveLoadedDocument; eller kör en engångsoperation som EncryptFile som läser en inmatning och skriver en utmatning. Oavsett vilket läser återställningen /XRefStm, slår ihop strömsektionen före de klassiska posterna, och löser upp de objekt som lever i strömmar innan skrivningen räknar upp dem. AES-256 krypteringsvägen är där problemet först visade sig, eftersom kryptering av ett dokument skriver om varje objekt och därmed kräver att varje objekt redan har lokaliserats

// One-shot: läs den hybrida indatafilen, skriv en AES-256 krypterad kopia
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Den detalj värd att ta med sig sitter uppströms om API:et. Filer som anländer från Word, Excel, PowerPoint och en lång rad "Spara som PDF"-pipelines är rutinmässigt hybrider, så en laddare som du bara testar mot din egen generators utdata kanske aldrig möter en vid testning. Fröa dina fixturer (fixtures) med dokument exporterade från riktiga Office-applikationer, inte bara med filer din egen kod producerade

Att kontrollera en fil du misstänker

Två inspektioner avgör frågan snabbt. Öppna filen i en hexvy och läs byten efter den sista startxref; en hybridfil visar en kort klassisk sektion vars trailer-dictionary innehåller /XRefStm. Eller jämför det objektantal en fullständig tolkning rapporterar med det högsta objektnumret som /Size deklarerar i trailern. Ett stort gap betyder att objekt gömmer sig i strömmar som laddaren inte har öppnat, vilket är samma brist som förvandlas till ett spar-misslyckande senare

Slutet på en typisk Excel-export gör den första kontrollen konkret. Allt efter det sista xref-nyckelordet är ren ASCII, så signaturen är läsbar direkt ut ur en hexvy (offsets illustrativa, kommentarer tillagda)

xref
0 0                          % tom klassisk undersektion: inga rader alls
trailer
<< /Size 216                 % ett förbi det högsta objektnumret som används
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % byteoffset för korsreferensströmmen
>>
startxref
88710                        % pekar på den klassiska sektionen ovan
%%EOF

0 0-undersektionen är ledtråden: en klassisk tabell med noll poster existerar enbart för att bära trailern, och trailern existerar främst för att säga /XRefStm 87325. En detektor som stannar vid xref-nyckelordet har, vid denna punkt, sett ett index på ingenting. När du hellre skriptar kontrollen än synar den, sitter markören alltid inom de sista par kilobyten av filen, så en begränsad bakåtläsning räcker

// Returnerar /XRefStm-offseten från filens slut (tail), eller -1 om 
// markören saknas (filen är inte en hybrid, eller inte en PDF alls)
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;                        // trailern lever i slutet
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // avgränsad bakåtläsning: 2 KB max
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // ingen hybridmarkör i slutet
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // hoppa över blanksteg efter nyckeln
  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;

// Användning: ett icke-negativt resultat namnger den byte där strömmen börjar
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hybridreferensfil: spara om kommer att behöva /XRefStm-sektionen');

Behandla sonden (proben) som triage, inte som en parser: den berättar vilka filer i en batch som förtjänar uppmärksamhet innan ett omsparningsjobb körs, och inget mer. Vad en laddare sedan måste göra med den offset den hittar, följa sektionskedjan, slå ihop strömposterna före de klassiska, respektera lediga-poster-vaktposterna, gås igenom steg för steg i vår systerartikel om hantering av hybridreferens-PDF-filer från Office-applikationer

Författarens sida av den här historien, hur objektströmmar och komprimerade korsreferenser produceras i första hand, täcks i vår artikel om objektströmmar och inkrementella uppdateringar. När hybridfilen i fråga också är mycket stor, låter laddningsteknikerna i Direct File API-genomgången för stora PDF-arbetsflöden dig inspektera den utan att läsa in hela saken i minnet. Båda paras naturligt med den återställning som beskrivs här, som levereras som del av HotPDF Component för Delphi och C++Builder vid sidan av de laddnings-, redigerings-, krypterings- och signerings-API:er som täcks på andra ställen i den här bloggen