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