Teknisk artikel

Varför vissa PDF-objektströmmar avkodas till skräp i Delphi

En PDF-objektström som packas upp utan fel men fortfarande läses som brus saknar vanligtvis ett steg: att vända ISO 32000-1-Predictorn. När en ströms /DecodeParms-ordbok bär /Predictor 2 eller högre är byten FlateDecode ger tillbaka inte originaldatan — de är PNG-stils raddifferentierade eller TIFF-stils horisontellt differentierade värden som behöver en andra rekonstruktionspassering innan någon ordboksuppslagning är meningsfull. PDFiumPas, det nativa VCL PDF-komponentbiblioteket för Delphi och C++Builder, lade till den rekonstruktionspasseringen i v2.16.0, specifikt eftersom PDF 1.5+-objektströmmar expanderade till differentierade byte som ingen ordboksparser kunde läsa

Varför FlateDecode ensamt inte räcker

FlateDecode självt är bara DEFLATE-dekomprimering (ISO 32000-1 §7.4.4.1): den återskapar vilka byte kodaren än gav till komprimeraren, inget mer. Predictorn finns ett lager upp, i strömmens /DecodeParms-ordbok, och den beskriver en transform kodaren tillämpade innan komprimering — differentiering förvandlar långa körer av liknande strukturerade värden, som de tätt packade heltalen inuti en korsreferensström eller en objektström, till långa körer av små tal som DEFLATE komprimerar mycket bättre. ISO 32000-1 §7.4.4.3 (Tabell 8) är tydlig med att ångra den här transformen är en del av att avkoda en filtrerad ström, inte en valfri städpassering, ändå är det lätt att skriva en FlateDecode-hjälpare som bara anropar inflate och stannar där

Symtomet är distinkt när man väl vet att leta efter det. Predictor-differentierade byte är inte slumpmässigt brus — de bär fortfarande formen av en komprimerad ström, så en naiv parser går ofta förbi några giltigt utseende tokens innan den träffar en bytesekvens som omöjligt kan vara ett PDF-namn, tal, eller avgränsare, och olika rader misslyckas vid olika offsetar beroende på hur mycket de underliggande värdena råkade skilja sig från sina grannar. Den inkonsekvensen är vad som gör buggen svår att fastställa från en enskild misslyckad fil: två PDF:er från samma producent kan skilja sig bara i vilka värden som råkar upprepas, så en tolkas nästan av misstag medan den andra misslyckas helt

Vad gör PDF Predictor-parametern egentligen?

/Predictor-posten i /DecodeParms talar om för en konform läsare vilken omvändning som ska köras, och ISO 32000-1 Tabell 8 definierar värdena som spelar roll i praktiken: 1 betyder att ingen prediktion tillämpades, 2 väljer TIFF Predictor 2 (horisontell differentiering), och alla värden från 10 till 15 väljer PNG-stils prediktion. Tre till nycklar reser tillsammans med den — /Colors, /BitsPerComponent, och /Columns — och tillsammans beskriver de radgeometrin differentieringen beräknades mot, även när strömmen inte innehåller någon bilddata alls: en objektström är inte en bild, men PDF-skrivare återanvänder samma radbaserade prediktormaskineri för den eftersom delta-sedan-deflate komprimerar tätt packade heltal och objektoffsetar bättre än att deflatera dem råa

TIFF Predictor 2 är det enklare av de två schemana: varje komponent lagras som differensen från samma komponent i föregående pixel på samma rad, och varje rad återställs vid sin vänsterkant snarare än att bära in en differens från raden ovanför. PNG-prediktion är mer speciell, eftersom det faktiska filtret kan ändras från rad till rad: varje rad börjar med en enda taggbyte — 0 för None, 1 för Sub, 2 för Up, 3 för Average, 4 för Paeth — och den taggen, inte det deklarerade /Predictor-värdet, avgör hur just den raden rekonstrueras. Ett /Predictor på 12 är egentligen bara kodarens antydan om att den favoriserade Up-filtret, där varje byte återställs genom att lägga till byten direkt ovanför den i föregående rad, men en korrekt avkodare måste ändå läsa taggen på varje rad snarare än att anta Up genomgående

Varför gör objektströmmar en missad Predictor osynlig?

Objektströmmar förvärrar problemet istället för att bara upprepa det. ISO 32000-1 §7.5.7 låter en PDF 1.5+-skrivare packa flera indirekta objekt i en enda komprimerad behållare, en /ObjStm, och det är vanligt att just de objekt en validerare mest behöver — katalogen, /OutputIntents, eller en XMP-/Metadata-ström — reser genom den behållaren med /Predictor 12 bifogat, eftersom de objekten är korta och repetitiva nog att dra nytta av raddifferentiering. När prediktorsteget saknas kastar expandering av objektströmmen inget fel: den producerar en bytesekvens som ser ytligt trovärdig ut men inte tokeniseras till de förväntade objekten, så vad som än packades inuti dyker helt enkelt inte upp. Rendrering märker sällan detta, eftersom en konform rendreringsmotor redan rekonstruerar prediktor-differentierad data innan den någonsin kommer till layout; koden som märker det är precis den typ den här buggen gömde sig inuti — en validerare, signerare, eller versionskontrollant som går igenom de råa PDF-byten själv för att svara på en strukturell fråga, utan någon reservlösning när dess egen vy av objektströmmen kommer tillbaka fel

PDFiumPas råkade ut för precis det här felet innan v2.16.0. Objektströmmar byggda med /Predictor 12, det vanliga fallet för PDF 1.5+-skrivare, expanderade genom PdfExpandObjectStreams till differentierade byte som den strukturella skannern inte kunde tolka, så katalogen, /OutputIntents, och /Metadata-objekt packade inuti var effektivt osynliga för konformitetsskanningar — inget undantag, ingen varning, bara en skanning som tyst betedde sig som om de objekten var frånvarande. De djupare mekanikerna för hur PDFiumPas löser upp en objektström mot den aktiva korsreferenstabellen, inklusive hybrid- och rena xref-strömfallen, täcks separat i artikeln om att validera objekt- och xref-strömmar med PDFiumPas; prediktorsteget som beskrivs här körs efter den upplösningen, på byten varje komprimerat objekt faktiskt innehåller

Att vända PNG- och TIFF-Predictor-rader i Pascal

PDFiumPas vänder differentieringen i en enda rutin, PdfApplyPredictor, och dess geometrimatematik är värd att känna till oavsett om du anropar den eller återimplementerar idén i din egen Delphi-kod. Radbredden i byte är ceil(Columns × Colors × BitsPerComponent ÷ 8) och per-pixel-bytebredden båda algoritmerna använder är ceil(Colors × BitsPerComponent ÷ 8) — få endera avrundningen fel och rekonstruktionen läser över en radgräns istället för inom en. Ett /Predictor under 2 lämnas orört, eftersom 1 betyder att kodaren inte tillämpade någon transform alls; 2 väljer TIFF-grenen visad nedan, och allt från 10 och uppåt faller igenom till PNG-radfilterrekonstruktion, där taggbyten i början av varje rad — inte det deklarerade /Predictor-värdet — avgör hur just den raden ångras

function PdfApplyPredictor(const Src: TBytes;
  Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
  RowLen, Bpp, R, I: Integer;
begin
  Result:= Src;
  if Predictor< 2 then
    Exit;                                   // 1 = no prediction, nothing to undo
  if Colors<= 0 then Colors:= 1;
  if Bpc<= 0 then Bpc:= 8;
  if Columns<= 0 then Columns:= 1;
  if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
    Exit;                                   // reject hostile row geometries
  RowLen:= (Columns* Colors* Bpc+ 7) div 8;  // ceil(), per ISO 32000-1 Table 8
  Bpp:= (Colors* Bpc+ 7) div 8;
  if Predictor= 2 then
  begin
    if Bpc<> 8 then
      Exit;                                 // only the 8-bit layout is reconstructed
    Result:= Copy(Src, 0, Length(Src));
    R:= 0;
    while R+ RowLen<= Length(Result) do
    begin
      for I:= R+ Bpp to R+ RowLen- 1 do
        Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
      Inc(R, RowLen);
    end;
    Exit;
  end;
  // Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
  SrcOfs:= R* (RowLen+ 1);
  DstOfs:= R* RowLen;
  Tag:= Src[SrcOfs];
  Inc(SrcOfs);
  for I:= 0 to RowLen- 1 do
  begin
    if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0;   // byte to the left
    if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0;   // byte above
    case Tag of
    1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A);            // Sub
    2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B);            // Up
    3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
    // Paeth (tag 4) adds whichever of A, B or the byte above-left sits
    // closest to the linear predictor A+ B- C; tag 0 (None) copies the
    // filtered byte through unchanged
    else Result[DstOfs+ I]:= Src[SrcOfs+ I];
    end;
  end;
end;

Vad PDFiumPas ändrade i v2.16.0

Fixen som levererades i PDFiumPas v2.16.0 sitter inuti PdfReadAndDecodeStream, rutinen som läser en strömsråa byte och avkodar dem för varje anropare som behöver inspektera PDF-struktur på bytenivå, inklusive objektströmexpansion; den försöker bara rekonstruktion efter att ha bekräftat att /Filter är ett rent FlateDecode, aldrig en kaskad, eftersom ett kedjat filter inte säkert kan prediktor-korrigeras på det här lagret. Att läsa tillbaka /Predictor, /Colors, /BitsPerComponent, och /Columns ur strömordboken behöver heller ingen generell ordboksparser: PdfDictRefNum hittar varje nyckel genom direkt namntoken-sökning inom den enskilda ordbokens byteintervall, vilket är säkert här just eftersom de fyra nycklarna inte kan upprepas eller kapslas inuti en enda strömordbok. Samma namntoken-sökning är mycket riskablare när den väl pekas mot en större eller mindre avgränsad region av en PDF-fil, vilket är ämnet för följeartikeln om att tolka PDF-ordböcker säkert

// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
  Inflated:= PdfInflate(Raw);
  Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
  if Predictor>= 2 then
  begin
    PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
    PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
    PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
    Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
  end
  else
    Result:= Inflated;
end;

Före v2.16.0 expanderade en objektström byggd med /Predictor 12 till differentierade byte utan att något fel kastades, så alla katalog-, /OutputIntents-, eller /Metadata-objekt packade inuti den försvann från PDFiumPas strukturella skanningar utan någon varning. Efter fixen packas samma objektström upp och rekonstrueras sedan korrekt, och objekten packade inuti den blir synliga för de skanningarna igen. Defensiva gränser reste med fixen: PdfApplyPredictor avvisar nu /Colors över 64, /BitsPerComponent över 32, och /Columns över 2^24 rakt av, eftersom de kombinationerna beskriver radgeometrier ingen riktig PDF-producent behöver och existerar huvudsakligen för att få en avkodare att allokera mycket mer minne än indatabyten motiverar

var
  Pdf: TPdf;
  Report: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'incoming.pdf';
    Pdf.Active := True;
    Report := Pdf.ValidatePdfA;
    if not Report.IsCompliant then
      LogNonCompliance(Report); // caller-supplied handler
  finally
    Pdf.Free;
  end;
end;

Begränsningar värda att känna till

PDFiumPas prediktorrekonstruktion har två kanter värda att känna till innan du förlitar dig på den. TIFF Predictor 2-rekonstruktion täcker bara fallet med 8 bitar per komponent; PDF tillåter smalare packningar, men sub-byte TIFF-differentierad data passerar igenom orekonstruerad snarare än att gissas fram, så en ström som deklarerar /Predictor 2 med /BitsPerComponent 1, 2, eller 4 kommer inte att avkodas korrekt genom den här vägen idag. PNG-prediktion har ingen sådan begränsning — varje rad tillhandahåller sin egen filtertagg, och alla fem definierade typer rekonstrueras oavsett vad det deklarerade /Predictor-värdet mellan 10 och 15 än råkar vara, vilket stämmer med hur PNG-stilsfiltrering faktiskt fungerar: det deklarerade värdet ligger närmare en antydan om vad kodaren mestadels använde än ett löfte om varje rad

PDFiums nativa rendreringsmotor rekonstruerar redan prediktor-differentierad bild- och innehållsströmdata korrekt, vilket är precis varför en fil kan rendrera perfekt i vilken vanlig visare som helst medan en bytenivå-validerare, signerare, eller versionskontrollant byggd ovanpå den läser samma byte fel. Den prediktormedvetna avkodningen som beskrivs här backar upp PDF/A-valideringen, strukturella skanningen, och signeringsfunktionerna hos PDFiumPas, den nativa VCL PDFium-komponenten för Delphi och C++Builder