Teknisk artikkel

Hvorfor noen PDF-objektstrømmer dekodes til søppel i Delphi

En PDF-objektstrøm som pakkes ut uten feil, men fortsatt leser som støy, mangler vanligvis ett trinn: å reversere ISO 32000-1 Predictor-en. Når en strøms /DecodeParms-ordbok bærer /Predictor 2 eller høyere, er bytene FlateDecode gir tilbake, ikke de opprinnelige dataene — de er PNG-stil rad-differensierte eller TIFF-stil horisontalt differensierte verdier som trenger en andre rekonstruksjonspassering før noe ordbok-oppslag gir mening. PDFiumPas, det native VCL PDF-komponentbiblioteket for Delphi og C++Builder, la til den rekonstruksjonspasseringen i v2.16.0, spesifikt fordi PDF 1.5+ objektstrømmer utvidet seg til differensierte bytes ingen ordbok-parser kunne lese

Hvorfor FlateDecode alene ikke er nok

FlateDecode i seg selv er bare DEFLATE-dekomprimering (ISO 32000-1 §7.4.4.1): den reproduserer hvilke som helst bytes koderen ga til komprimereren, ikke noe mer. Predictor-en bor ett lag over, i strømmens /DecodeParms-ordbok, og den beskriver en transformasjon koderen anvendte før komprimering — differensiering gjør lange rekker av lignende strukturerte verdier, som de tettpakkede heltallene inne i en kryssreferansestrøm eller en objektstrøm, om til lange rekker av små tall DEFLATE komprimerer langt bedre. ISO 32000-1 §7.4.4.3 (Tabell 8) er eksplisitt på at å angre denne transformasjonen er en del av å dekode en filtrert strøm, ikke en valgfri opprydningspassering, men det er lett å skrive en FlateDecode-hjelper som bare kaller inflate og stopper der

Symptomet er distinkt når man vet å se etter det. Predictor-differensierte bytes er ikke tilfeldig støy — de bærer fortsatt formen på en komprimert strøm, så en naiv parser går ofte forbi noen gyldig-utseende tokens før den treffer en bytesekvens som umulig kan være et PDF-navn, tall, eller skilletegn, og forskjellige rader feiler ved forskjellige forskyvninger avhengig av hvor mye de underliggende verdiene tilfeldigvis skilte seg fra naboene sine. Den inkonsistensen er det som gjør bugen vanskelig å feste til en enkelt mislykket fil: to PDF-er fra samme produsent kan bare skille seg i hvilke verdier som tilfeldigvis gjentar seg, slik at én parser nesten ved en tilfeldighet mens den andre feiler rett ut

Hva gjør PDF Predictor-parameteren egentlig?

/Predictor-oppføringen i /DecodeParms forteller en konform leser hvilken reversering som skal kjøres, og ISO 32000-1 Tabell 8 definerer verdiene som betyr noe i praksis: 1 betyr at ingen prediksjon ble anvendt, 2 velger TIFF Predictor 2 (horisontal differensiering), og enhver verdi fra 10 til 15 velger PNG-stil prediksjon. Tre nøkler til reiser sammen med den — /Colors, /BitsPerComponent, og /Columns — og sammen beskriver de rad-geometrien differensieringen ble beregnet mot, selv når strømmen ikke holder noen bildedata i det hele tatt: en objektstrøm er ikke et bilde, men PDF-skrivere gjenbruker det samme rad-baserte predictor-maskineriet for det fordi delta-så-deflate komprimerer tettpakkede heltall og objektforskyvninger bedre enn å deflate dem rått

TIFF Predictor 2 er den enklere av de to skjemaene: hver komponent lagres som differansen fra den samme komponenten i forrige piksel på samme rad, og hver rad tilbakestilles ved sin venstre kant i stedet for å bære en differanse inn fra raden over. PNG-prediksjon er mer nøye, fordi selve filteret kan endre seg fra rad til rad: hver rad starter med én enkelt tag-byte — 0 for None, 1 for Sub, 2 for Up, 3 for Average, 4 for Paeth — og den tagen, ikke den deklarerte /Predictor-verdien, avgjør hvordan nettopp den raden rekonstrueres. En /Predictor på 12 er egentlig bare koderens hint om at den foretrakk Up-filteret, der hver byte gjenopprettes ved å legge til byten rett over den i forrige rad, men en korrekt dekoder må fortsatt lese tagen på hver rad i stedet for å anta Up gjennomgående

Hvorfor gjør objektstrømmer en glemt Predictor usynlig?

Objektstrømmer forverrer problemet i stedet for bare å gjenta det. ISO 32000-1 §7.5.7 lar en PDF 1.5+-skriver pakke flere indirekte objekter inn i én enkelt komprimert container, en /ObjStm, og det er vanlig at nettopp objektene en validator trenger mest — katalogen, /OutputIntents, eller en XMP-/Metadata-strøm — reiser gjennom den containeren med /Predictor 12 festet, fordi de objektene er korte og repetitive nok til å nyte godt av rad-differensiering. Når predictor-trinnet mangler, kaster ikke utvidelse av objektstrømmen en feil: den produserer en bytesekvens som ser overfladisk plausibel ut, men som ikke tokeniserer til de forventede objektene, så hva som enn var pakket inni, dukker rett og slett ikke opp. Gjengivelse legger sjelden merke til det, fordi en konform gjengivelsesmotor allerede rekonstruerer predictor-differensierte data før det noensinne kommer til layout; koden som legger merke til det, er nøyaktig den typen denne bugen gjemte seg inne i — en validator, signerer, eller versjonssjekker som selv går gjennom de rå PDF-bytene for å svare på et strukturelt spørsmål, uten noen reserveløsning når dens eget syn på objektstrømmen kommer tilbake feil

PDFiumPas traff nøyaktig denne feilen før v2.16.0. Objektstrømmer bygget med /Predictor 12, det vanlige tilfellet for PDF 1.5+-skrivere, utvidet seg gjennom PdfExpandObjectStreams til differensierte bytes strukturskanneren ikke kunne parse, så katalogen, /OutputIntents, og /Metadata-objektene pakket inni, var effektivt usynlige for konformitetsskanninger — intet unntak, ingen advarsel, bare en skanning som stille oppførte seg som om de objektene var fraværende. De dypere mekanikkene i hvordan PDFiumPas løser en objektstrøm mot den aktive kryssreferansetabellen, inkludert hybrid- og rene xref-strøm-tilfellene, dekkes separat i artikkelen om å validere objekt- og xref-strømmer med PDFiumPas; predictor-trinnet beskrevet her kjører etter den løsningen, på bytene hvert komprimerte objekt faktisk inneholder

Å reversere PNG- og TIFF-Predictor-rader i Pascal

PDFiumPas reverserer differensieringen i én enkelt rutine, PdfApplyPredictor, og geometrimatematikken dens er verdt å kjenne til enten man kaller den eller reimplementerer idéen i sin egen Delphi-kode. Radbredden i bytes er ceil(Columns × Colors × BitsPerComponent ÷ 8), og per-piksel-bytebredden begge algoritmene bruker, er ceil(Colors × BitsPerComponent ÷ 8) — få enten avrundingen feil, og rekonstruksjonen leser på tvers av en radgrense i stedet for innenfor én. En /Predictor under 2 lates urørt, ettersom 1 betyr at koderen ikke anvendte noen transformasjon i det hele tatt; 2 velger TIFF-grenen vist nedenfor, og alt fra 10 og oppover faller gjennom til PNG-rad-filter-rekonstruksjon, der tag-byten ved starten av hver rad — ikke den deklarerte /Predictor-verdien — avgjør hvordan nettopp den raden angres

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;

Hva PDFiumPas endret i v2.16.0

Fiksen som ble levert i PDFiumPas v2.16.0, sitter inne i PdfReadAndDecodeStream, rutinen som leser en strøms rå bytes og dekoder dem for hver kaller som trenger å inspisere PDF-struktur på byte-nivå, inkludert objektstrøm-utvidelse; den forsøker bare rekonstruksjon etter å ha bekreftet at /Filter er en ren FlateDecode, aldri en kaskade, fordi et kjedet filter ikke trygt kan predictor-korrigeres på dette laget. Å lese /Predictor, /Colors, /BitsPerComponent, og /Columns tilbake ut av strømordboken trenger heller ingen generell ordbok-parser: PdfDictRefNum finner hver nøkkel ved direkte navn-token-søk innenfor den ene ordbokens byte-rekkevidde, noe som er trygt her nettopp fordi de fire nøklene ikke kan gjentas eller nøstes inne i én enkelt strømordbok. Det samme navn-token-søket er langt mer risikabelt når det rettes mot et større eller mindre avgrenset område av en PDF-fil, noe som er temaet for følgeartikkelen om å parse PDF-ordbøker trygt

// 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 utvidet en objektstrøm bygget med /Predictor 12 seg til differensierte bytes uten at noen feil ble kastet, så ethvert katalog-, /OutputIntents-, eller /Metadata-objekt pakket inni, forsvant fra PDFiumPas' strukturskanninger uten noen advarsel. Etter fiksen pakkes den samme objektstrømmen ut og rekonstrueres deretter korrekt, og objektene pakket inni blir synlige for de skanningene igjen. Defensive grenser fulgte med fiksen: PdfApplyPredictor avviser nå /Colors over 64, /BitsPerComponent over 32, og /Columns over 2^24 rett ut, fordi de kombinasjonene beskriver rad-geometrier ingen ekte PDF-produsent trenger, og eksisterer hovedsakelig for å få en dekoder til å allokere langt mer minne enn inndata-bytene rettferdiggjø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;

Grenser verdt å kjenne til

PDFiumPas' predictor-rekonstruksjon har to kanter verdt å kjenne til før man stoler på den. TIFF Predictor 2-rekonstruksjon dekker bare 8-bit-per-komponent-tilfellet; PDF tillater smalere pakninger, men sub-byte TIFF-differensierte data passerer gjennom urekonstruert i stedet for å bli gjettet på, så en strøm som deklarerer /Predictor 2 med /BitsPerComponent 1, 2, eller 4, vil ikke dekode korrekt gjennom denne veien i dag. PNG-prediksjon har ingen slik begrensning — hver rad leverer sin egen filter-tag, og alle fem definerte typer rekonstrueres uansett hva den deklarerte /Predictor-verdien mellom 10 og 15 tilfeldigvis er, noe som stemmer med hvordan PNG-stil filtrering faktisk fungerer: den deklarerte verdien er nærmere et hint om hva koderen mest brukte enn et løfte om hver rad

PDFiums native gjengivelsesmotor rekonstruerer allerede predictor-differensierte bilde- og innholdsstrøm-data korrekt, noe som er nøyaktig grunnen til at en fil kan gjengis perfekt i enhver vanlig fremviser mens en byte-nivå-validator, -signerer, eller -versjonssjekker bygget oppå den, leser de samme bytene feil. Den predictor-bevisste dekodingen beskrevet her støtter PDF/A-valideringen, strukturskanningen, og signerings-funksjonene til PDFiumPas, den native VCL PDFium-komponenten for Delphi og C++Builder