Tekninen artikkeli

Miksi jotkin PDF-oliovirrat dekoodautuvat roskaksi Delphissä

PDF-oliovirta, joka pumppautuu auki virheettä mutta lukee silti kohinana, puuttuu yleensä yhden vaiheen: ISO 32000-1 Predictorin kääntämisen. Kun virran /DecodeParms-sanakirja kantaa /Predictor-arvon 2 tai suuremman, tavut, jotka FlateDecode antaa takaisin, eivät ole alkuperäistä dataa — ne ovat PNG-tyylisiä rivierotettuja tai TIFF-tyylisiä vaakasuunnassa erotettuja arvoja, jotka tarvitsevat toisen rekonstruktiokierroksen ennen kuin mikään sanakirjahaku on järkevä. PDFiumPas, natiivi VCL-PDF-komponenttikirjasto Delphille ja C++Builderille, lisäsi tuon rekonstruktiokierroksen v2.16.0:ssa, nimenomaan koska PDF 1.5+ -oliovirrat pumppautuivat auki eroteltuina tavuina, joita mikään sanakirjajäsennin ei pystynyt lukemaan

Miksi pelkkä FlateDecode ei riitä

FlateDecode itse on vain DEFLATE-purku (ISO 32000-1 §7.4.4.1): se toistaa mitkä tahansa tavut, jotka koodain antoi pakkaimelle, ei mitään muuta. Predictor asuu yhden kerroksen ylempänä, virran /DecodeParms-sanakirjassa, ja se kuvaa muunnoksen, jonka koodain sovelsi ennen pakkausta — erottelu muuttaa pitkät jaksot samankaltaisia rakenteisia arvoja, kuten tiiviisti pakattuja kokonaislukuja ristiviittausvirran tai oliovirran sisällä, pitkiksi jaksoiksi pieniä lukuja, jotka DEFLATE pakkaa paljon paremmin. ISO 32000-1 §7.4.4.3 (taulukko 8) on suora siitä, että tämän muunnoksen purkaminen on osa suodatetun virran dekoodaamista, ei valinnainen siivouskierros, mutta silti on helppo kirjoittaa FlateDecode-apufunktio, joka vain kutsuu inflate-purkua ja pysähtyy siihen

Oire on tunnistettava heti, kun tietää etsiä sitä. Predictor-eroteltavat tavut eivät ole satunnaista kohinaa — ne kantavat silti pakatun virran muotoa, joten naiivi jäsennin usein kävelee muutaman pätevän näköisen tokenin ohi ennen kuin osuu tavusekvenssiin, joka ei voi mitenkään olla PDF-nimi, luku tai erotin, ja eri rivit epäonnistuvat eri siirtymissä riippuen siitä, kuinka paljon taustalla olevat arvot sattuivat eroamaan naapureistaan. Tuo epäjohdonmukaisuus on se, mikä tekee bugin vaikeaksi jäljittää yhdestä epäonnistuvasta tiedostosta: kaksi PDF:ää samalta tuottajalta voivat erota vain siinä, mitkä arvot sattuvat toistumaan, joten toinen jäsentyy lähes vahingossa, kun taas toinen epäonnistuu täysin

Mitä PDF Predictor -parametri todella tekee?

/Predictor-merkintä /DecodeParms:ssä kertoo standardinmukaiselle lukijalle, minkä kääntämisen ajaa, ja ISO 32000-1 taulukko 8 määrittelee arvot, jotka ovat merkityksellisiä käytännössä: 1 tarkoittaa, ettei mitään ennustusta sovellettu, 2 valitsee TIFF Predictor 2:n (vaakasuora erottelu), ja mikä tahansa arvo 10:stä 15:een valitsee PNG-tyylisen ennustuksen. Kolme muuta avainta matkustaa sen rinnalla — /Colors, /BitsPerComponent ja /Columns — ja yhdessä ne kuvaavat rivigeometrian, jota vasten erottelu laskettiin, jopa silloin, kun virrassa ei ole lainkaan kuvadataa: oliovirta ei ole kuva, mutta PDF-kirjoittajat käyttävät uudelleen samaa rivipohjaista ennustinkoneistoa sille, koska delta-sitten-deflate pakkaa tiiviisti pakattuja kokonaislukuja ja olio-siirtymiä tiiviimmin kuin niiden raaka deflatoiminen

TIFF Predictor 2 on kahdesta järjestelmästä yksinkertaisempi: jokainen komponentti tallennetaan erotuksena samasta komponentista saman rivin edellisessä pikselissä, ja jokainen rivi nollautuu vasemmasta reunastaan sen sijaan, että kantaisi erotusta yläpuolisesta rivistä. PNG-ennustus on tarkempi, koska todellinen suodatin voi vaihtua rivistä toiseen: jokainen rivi alkaa yhdellä tunnistetavulla — 0 None:lle, 1 Sub:lle, 2 Up:lle, 3 Average:lle, 4 Paeth:lle — ja tuo tunniste, ei ilmoitettu /Predictor-arvo, päättää, miten juuri tuo rivi rekonstruoidaan. /Predictor-arvo 12 on todella vain koodaimen vihje siitä, että se suosi Up-suodatinta, jossa jokainen tavu palautetaan lisäämällä siihen suoraan yläpuolella oleva tavu edellisellä rivillä, mutta oikean dekooderin on silti luettava tunniste jokaiselta riviltä sen sijaan, että olettaisi Up:n kauttaaltaan

Miksi oliovirrat tekevät puuttuvasta Predictorista näkymättömän?

Oliovirrat pahentavat ongelmaa sen toistamisen sijaan. ISO 32000-1 §7.5.7 antaa PDF 1.5+ -kirjoittajalle mahdollisuuden pakata useita epäsuoria olioita yhteen pakattuun säiliöön, /ObjStm:ään, ja on yleistä, että juuri ne oliot, joita validaattori eniten tarvitsee — katalogi, /OutputIntents tai XMP /Metadata-virta — matkustavat tuon säiliön läpi /Predictor 12 liitettynä, koska nuo oliot ovat riittävän lyhyitä ja toistuvia hyötyäkseen rivierottelusta. Kun predictor-vaihe puuttuu, oliovirran laajentaminen ei nosta virhettä: se tuottaa tavusekvenssin, joka näyttää pinnallisesti uskottavalta mutta ei tokenisoidu odotetuiksi olioiksi, joten mitä tahansa sen sisään pakattiin, ei yksinkertaisesti ilmesty. Renderöinti harvoin huomaa tätä, koska standardinmukainen renderöintimoottori jo rekonstruoi predictor-eroteltua dataa ennen kuin se koskaan pääsee asetteluun; koodi, joka huomaa, on juuri sitä tyyppiä, jonka sisään tämä bugi piiloutui — validaattori, allekirjoittaja tai versiotarkistin, joka käy läpi raa'at PDF-tavut itse vastatakseen rakenteelliseen kysymykseen, ilman varakeinoa heti, kun sen oma näkemys oliovirrasta palautuu vääränä

PDFiumPas osui juuri tähän vikaan ennen v2.16.0:aa. Oliovirrat, jotka rakennettiin /Predictor 12:lla, yleinen tapaus PDF 1.5+ -kirjoittajille, laajenivat funktion PdfExpandObjectStreams kautta eroteltuiksi tavuiksi, joita rakenteellinen skanneri ei pystynyt jäsentämään, joten katalogi-, /OutputIntents- ja /Metadata-oliot, jotka oli pakattu sisään, olivat käytännössä näkymättömiä yhdenmukaisuusskannauksille — ei poikkeusta, ei varoitusta, vain skannaus, joka hiljaa käyttäytyi kuin nuo oliot olisivat olleet poissa. PDFiumPas:n syvemmät mekanismit siitä, miten se ratkaisee oliovirran aktiivista ristiviittaustaulua vasten, mukaan lukien hybridi- ja puhtaat xref-virtatapaukset, käsitellään erikseen artikkelissa olio- ja xref-virtojen validointi PDFiumPas:lla; tässä kuvattu predictor-vaihe ajetaan tuon ratkaisun jälkeen, tavuille, jotka kukin pakattu olio todella sisältää

PNG- ja TIFF Predictor -rivien kääntäminen Pascalissa

PDFiumPas kääntää erottelun yhdessä rutiinissa, PdfApplyPredictor, ja sen geometrialaskenta kannattaa tuntea, kutsutpa sitä tai toteutat idean uudelleen omassa Delphi-koodissasi. Rivin leveys tavuina on ceil(Columns × Colors × BitsPerComponent ÷ 8), ja pikselikohtainen tavuleveys, jota molemmat algoritmit käyttävät, on ceil(Colors × BitsPerComponent ÷ 8) — saa kumpi tahansa pyöristys väärin, ja rekonstruktio lukee rivin rajan yli sen sisällä pysymisen sijaan. /Predictor-arvo alle 2 jätetään koskematta, koska 1 tarkoittaa, ettei koodain soveltanut lainkaan muunnosta; 2 valitsee alla näkyvän TIFF-haaran, ja mikä tahansa 10:stä ylöspäin putoaa PNG-rivisuodatinrekonstruktioon, jossa kunkin rivin alussa oleva tunnistetavu — ei ilmoitettu /Predictor-arvo — päättää, miten juuri tuo rivi puretaan

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;

Mitä PDFiumPas muutti v2.16.0:ssa

Korjaus, joka julkaistiin PDFiumPas v2.16.0:ssa, istuu funktion PdfReadAndDecodeStream sisällä, rutiinissa, joka lukee virran raa'at tavut ja dekoodaa ne jokaiselle kutsujalle, joka tarvitsee tarkastaa PDF-rakenteen tavutasolla, mukaan lukien oliovirran laajennus; se yrittää rekonstruktiota vasta vahvistettuaan, että /Filter on paljas FlateDecode, ei koskaan ketjutus, koska ketjutettua suodinta ei voi turvallisesti predictor-korjata tällä kerroksella. /Predictor-, /Colors-, /BitsPerComponent- ja /Columns-arvojen lukeminen takaisin virtasanakirjasta ei myöskään tarvitse yleistä sanakirjajäsennintä: PdfDictRefNum löytää jokaisen avaimen suoralla nimitoken-haulla tuon yhden sanakirjan tavualueen sisällä, mikä on turvallista täällä juuri siksi, että nuo neljä avainta eivät voi toistua tai sisäkkäistyä yhden virtasanakirjan sisällä. Sama nimitoken-haku on paljon riskialttiimpi heti, kun se osoitetaan suurempaan tai vähemmän rajattuun PDF-tiedoston alueeseen, mikä on aihe artikkelissa rinnakkaisartikkeli PDF-sanakirjojen turvallisesta jäsentämisestä

// 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;

Ennen v2.16.0:aa, oliovirta, joka rakennettiin /Predictor 12:lla, laajeni eroteltuina tavuina ilman että virhettä nostettiin, joten mikä tahansa katalogi-, /OutputIntents- tai /Metadata-olio, joka oli pakattu sen sisään, katosi PDFiumPas:n rakenteellisista skannauksista ilman mitään varoitusta. Korjauksen jälkeen sama oliovirta pumppautuu auki ja rekonstruoituu sitten oikein, ja sen sisään pakatut oliot tulevat jälleen näkyviksi noille skannauksille. Puolustavat rajat kulkivat korjauksen mukana: PdfApplyPredictor hylkää nyt suoralta kädeltä /Colors-arvot yli 64:n, /BitsPerComponent-arvot yli 32:n ja /Columns-arvot yli 2^24:n, koska nuo yhdistelmät kuvaavat rivigeometrioita, joita mikään todellinen PDF-tuottaja ei tarvitse, ja ovat olemassa lähinnä saadakseen dekooderin varaamaan paljon enemmän muistia kuin syötetavut oikeuttavat

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;

Rajat, jotka kannattaa tuntea

PDFiumPas:n predictor-rekonstruktiolla on kaksi reunaa, jotka kannattaa tuntea ennen kuin luotat siihen. TIFF Predictor 2 -rekonstruktio kattaa vain 8-bittisen komponentin per komponentti -tapauksen; PDF sallii kapeampia pakkauksia, mutta tavun alle jäävä TIFF-eroteltu data kulkee läpi rekonstruoimattomana sen sijaan, että sitä arvattaisiin, joten virta, joka ilmoittaa /Predictor 2:n /BitsPerComponent-arvolla 1, 2 tai 4, ei dekoodaudu oikein tämän polun kautta tänään. PNG-ennustuksella ei ole tällaista rajoitusta — jokainen rivi tarjoaa oman suodatintunnisteensa, ja kaikki viisi määriteltyä tyyppiä rekonstruoidaan riippumatta siitä, mikä ilmoitettu /Predictor-arvo 10:n ja 15:n välillä sattuu olemaan, mikä täsmää sen kanssa, miten PNG-tyylinen suodatus todella toimii: ilmoitettu arvo on lähempänä vihjettä siitä, mitä koodain enimmäkseen käytti, kuin lupausta jokaisesta rivistä

PDFiumin natiivi renderöintimoottori jo rekonstruoi predictor-eroteltua kuva- ja sisältövirtadataa oikein, mikä on juuri se, miksi tiedosto voi renderöityä täydellisesti missä tahansa tavallisessa katseluohjelmassa, kun taas sen päälle rakennettu tavutason validaattori, allekirjoittaja tai versiotarkistin lukee samat tavut väärin. Tässä kuvattu predictor-tietoinen dekoodaus tukee PDF/A-validointia, rakenteellista skannausta ja allekirjoitusominaisuuksia PDFiumPas:ssa, natiivissa VCL PDFium -komponentissa Delphille ja C++Builderille