Teknisk artikel

Hvorfor nogle PDF-objektstrømme dekoder til skrammel i Delphi

En PDF-objektstrøm, der udpakkes uden fejl, men stadig læses som støj, mangler som regel ét trin: at vende ISO 32000-1 Predictor'en om. Når en streams /DecodeParms-ordbog bærer /Predictor 2 eller højere, er de bytes, FlateDecode giver tilbage, ikke de originale data — de er PNG-stil-række-differentierede eller TIFF-stil-vandret-differentierede værdier, der har brug for et andet rekonstruktionspas, før noget ordbogsopslag giver mening. PDFiumPas, det native VCL-PDF-komponentbibliotek til Delphi og C++Builder, tilføjede det rekonstruktionspas i v2.16.0, specifikt fordi PDF 1.5+-objektstrømme udvidede til differentierede bytes, ingen ordbogs-parser kunne læse

Hvorfor FlateDecode alene ikke er nok

FlateDecode selv er kun DEFLATE-dekomprimering (ISO 32000-1 §7.4.4.1): den reproducerer, hvilke bytes encoderen end overdrog til kompressoren, intet mere. Predictor'en bor ét lag oppe, i streamens /DecodeParms-ordbog, og den beskriver en transformation, encoderen anvendte før komprimering — differentiering omdanner lange serier af lignende strukturerede værdier, som de tæt pakkede heltal inde i en kryds-reference-stream eller en objektstrøm, til lange serier af små tal, som DEFLATE komprimerer langt bedre. ISO 32000-1 §7.4.4.3 (Tabel 8) er eksplicit om, at at fortryde denne transformation er en del af at afkode en filtreret stream, ikke et valgfrit oprydningspas, men det er let at skrive en FlateDecode-hjælper, der kun kalder inflate og stopper der

Symptomet er tydeligt, når man ved, hvad man skal kigge efter. Predictor-differentierede bytes er ikke tilfældig støj — de bærer stadig formen af en komprimeret stream, så en naiv parser går ofte forbi et par gyldigt-udseende tokens, før den rammer en byte-sekvens, der umuligt kan være et PDF-navn, tal eller skilletegn, og forskellige rækker fejler ved forskellige forskydninger afhængigt af, hvor meget de underliggende værdier tilfældigvis afveg fra deres naboer. Den uoverensstemmelse er, hvad der gør bugen svær at fastslå fra en enkelt fejlende fil: to PDF'er fra samme producent kan kun adskille sig i, hvilke værdier der tilfældigvis gentager sig, så én parser næsten ved et tilfælde, mens den anden fejler direkte

Hvad gør PDF Predictor-parameteren rent faktisk?

/Predictor-posten i /DecodeParms fortæller en konform læser, hvilken omvending den skal køre, og ISO 32000-1 Tabel 8 definerer de værdier, der betyder noget i praksis: 1 betyder ingen forudsigelse blev anvendt, 2 vælger TIFF Predictor 2 (vandret differentiering), og enhver værdi fra 10 til 15 vælger PNG-stil-forudsigelse. Tre flere nøgler rejser sammen med den — /Colors, /BitsPerComponent og /Columns — og sammen beskriver de den række-geometri, differentieringen blev beregnet mod, selv når streamen ikke indeholder noget billeddata overhovedet: en objektstrøm er ikke et billede, men PDF-skrivere genbruger det samme række-baserede predictor-maskineri til den, fordi delta-så-deflate komprimerer tæt pakkede heltal og objekt-forskydninger bedre end at deflate dem rå

TIFF Predictor 2 er den simplere af de to skemaer: hver komponent gemmes som forskellen fra den samme komponent i den forrige pixel på den samme række, og hver række nulstiller ved sin venstre kant frem for at bære en forskel ind fra rækken ovenover. PNG-forudsigelse er mere kræsen, fordi det faktiske filter kan ændre sig fra række til række: hver række starter med en enkelt tag-byte — 0 for None, 1 for Sub, 2 for Up, 3 for Average, 4 for Paeth — og det tag, ikke den erklærede /Predictor-værdi, afgør, hvordan netop den række rekonstrueres. En /Predictor på 12 er egentlig bare encoderens hint om, at den foretrak Up-filteret, hvor hver byte gendannes ved at addere den byte direkte ovenover den i den forrige række, men en korrekt dekoder skal stadig læse tagget på hver række frem for at antage Up hele vejen igennem

Hvorfor gør objektstrømme en oversprunget Predictor usynlig?

Objektstrømme forstærker problemet frem for bare at gentage det. ISO 32000-1 §7.5.7 lader en PDF 1.5+-skriver pakke flere indirekte objekter ind i en enkelt komprimeret container, en /ObjStm, og det er almindeligt, at netop de objekter en validator har mest brug for — kataloget, /OutputIntents, eller en XMP-/Metadata-stream — rejser gennem den container med /Predictor 12 tilknyttet, fordi de objekter er korte og gentagende nok til at drage fordel af række-differentiering. Når predictor-trinnet mangler, kaster udvidelse af objektstrømmen ikke en fejl: den producerer en byte-sekvens, der ser overfladisk plausibel ud, men som ikke tokeniserer til de forventede objekter, så hvad end der var pakket indeni, dukker simpelthen ikke op. Gengivelse bemærker det sjældent, fordi en konform gengivelsesmotor allerede rekonstruerer predictor-differentierede data, før de nogensinde når layout; koden der bemærker det, er netop den slags, denne bug gemte sig i — en validator, signerer eller versions-tjekker, der gennemgår de rå PDF-bytes selv for at besvare et strukturelt spørgsmål, uden nogen fallback, når dens egen visning af objektstrømmen kommer tilbage forkert

PDFiumPas ramte netop denne fejl før v2.16.0. Objektstrømme bygget med /Predictor 12, det almindelige tilfælde for PDF 1.5+-skrivere, udvidede gennem PdfExpandObjectStreams til differentierede bytes, som den strukturelle skanner ikke kunne parse, så kataloget, /OutputIntents og /Metadata-objekter pakket indeni var effektivt usynlige for konformitetsskanninger — ingen undtagelse, ingen advarsel, bare en skanning der i stilhed opførte sig, som om de objekter var fraværende. De dybere mekanikker i, hvordan PDFiumPas løser en objektstrøm mod den aktive kryds-reference-tabel, inklusive de hybride og rene xref-stream-tilfælde, dækkes separat i artiklen om validering af objekt- og xref-streams med PDFiumPas; predictor-trinnet beskrevet her kører efter den løsning, på de bytes hvert komprimeret objekt rent faktisk indeholder

At vende PNG- og TIFF-Predictor-rækker om i Pascal

PDFiumPas vender differentieringen om i én enkelt rutine, PdfApplyPredictor, og dens geometri-matematik er værd at kende, uanset om man kalder den eller genimplementerer idéen i sin egen Delphi-kode. Række-bredden i bytes er ceil(Columns × Colors × BitsPerComponent ÷ 8), og pixel-bredden begge algoritmer bruger, er ceil(Colors × BitsPerComponent ÷ 8) — får man en af afrundingerne forkert, læser rekonstruktionen på tværs af en rækkegrænse i stedet for inden i én. En /Predictor under 2 lades urørt, da 1 betyder, at encoderen ikke anvendte nogen transformation overhovedet; 2 vælger TIFF-grenen vist nedenfor, og alt fra 10 og opefter falder igennem til PNG-række-filter-rekonstruktion, hvor tag-byten i starten af hver række — ikke den erklærede /Predictor-værdi — afgør, hvordan netop den række fortrydes

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;

Hvad PDFiumPas ændrede i v2.16.0

Fixen der blev sendt i PDFiumPas v2.16.0, sidder inde i PdfReadAndDecodeStream, rutinen der læser en streams rå bytes og dekoder dem til hver kalder, der har brug for at inspicere PDF-struktur på byte-niveau, inklusive objektstrøm-udvidelse; den forsøger kun rekonstruktion, efter at have bekræftet /Filter er en ren FlateDecode, aldrig en kaskade, fordi et kædet filter ikke sikkert kan predictor-korrigeres på dette lag. At læse /Predictor, /Colors, /BitsPerComponent og /Columns tilbage ud af stream-ordbogen kræver heller ikke en generel ordbogs-parser: PdfDictRefNum finder hver nøgle ved direkte navn-token-søgning inden for det ene ordbogs byte-interval, hvilket er sikkert her netop fordi de fire nøgler ikke kan gentages eller indlejres inde i en enkelt stream-ordbog. Den samme navn-token-søgning er langt mere risikabel, når den peges mod en større eller mindre-afgrænset region af en PDF-fil, hvilket er emnet for følgeartiklen om sikker parsing af PDF-ordbøger

// 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ør v2.16.0 udvidede en objektstrøm bygget med /Predictor 12 til differentierede bytes uden nogen fejl rejst, så ethvert katalog-, /OutputIntents- eller /Metadata-objekt pakket indeni forsvandt fra PDFiumPas' strukturelle skanninger uden nogen advarsel. Efter fixen udfolder den samme objektstrøm og rekonstruerer derefter korrekt, og objekterne pakket indeni bliver synlige for de skanninger igen. Defensive grænser rejste med fixen: PdfApplyPredictor afviser nu /Colors over 64, /BitsPerComponent over 32, og /Columns over 2^24 direkte, fordi de kombinationer beskriver række-geometrier, ingen rigtig PDF-producent har brug for, og hovedsageligt eksisterer for at få en dekoder til at allokere langt mere hukommelse, end inputbytene retfærdiggør

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;

Grænser værd at kende

PDFiumPas' predictor-rekonstruktion har to kanter, det er værd at kende, før man stoler på den. TIFF Predictor 2-rekonstruktion dækker kun 8-bit-pr.-komponent-tilfældet; PDF tillader smallere pakninger, men sub-byte-TIFF-differentieret data passerer igennem urekonstrueret frem for at blive gættet på, så en stream der erklærer /Predictor 2 med /BitsPerComponent 1, 2 eller 4, vil ikke dekode korrekt gennem denne vej i dag. PNG-forudsigelse har ingen sådan begrænsning — hver række leverer sin egen filter-tag, og alle fem definerede typer rekonstrueres uanset, hvad den erklærede /Predictor-værdi mellem 10 og 15 tilfældigvis er, hvilket matcher, hvordan PNG-stil-filtrering rent faktisk fungerer: den erklærede værdi er tættere på et hint om, hvad encoderen mest brugte, end et løfte om hver række

PDFiums native gengivelsesmotor rekonstruerer allerede predictor-differentieret billed- og content-stream-data korrekt, hvilket er præcis grunden til, at en fil kan gengives perfekt i enhver almindelig fremviser, mens en byte-niveau-validator, -signerer eller -versions-tjekker bygget oven på den læser de samme bytes forkert. Den predictor-bevidste afkodning beskrevet her bakker PDF/A-validerings-, strukturel-skannings- og signerings-funktionerne i PDFiumPas, den native VCL-PDFium-komponent til Delphi og C++Builder