Tekninen artikkeli

PDF-ennustimen ja LZWDecode-dekoodauksen Delphi-toteutus HotPDF:ssä

/Predictor 12-merkitty virta ei tarkoita, että jokainen rivi käyttää PNG-suodinta 2. HotPDF, natiivi VCL PDF -komponentti Delphille ja C++Builderille, käsittelee ennustimen arvot 10–15 yhtenä perheenä: todellinen suodintagi, 0–4, on jokaisen koodatun rivin ensimmäinen tavu, ja HPDFDecodePredictor lukee ja validoi tuon tagin rivi riviltä. Tuo ero on lähes jokaisen bugin muoto tässä PDF:n kolkassa, koska mikään ei nosta virhettä, kun teet sen väärin. Suodinketju ajaa, rasteri on odottamasi kokoinen, ja kuva tulee ulos vinona kohinana tai gradienttina, joka ajautuu kauemmas jokaisella skannausrivillä. Viisi lukua kohdassa /DecodeParms (ISO 32000-1 §7.4.4) enimmäkseen muuttavat tavujen merkitystä eivätkä niiden pituutta, joten väärä arvo tuottaa uskottavaa roskaa virheen sijaan

Miksi /Predictor 12 ei tarkoita PNG-suodinta 2 jokaisella rivillä?

Koska ennustinnumero vain sanoo "PNG-ennustus on käytössä", ei mitä suodinta. PNG-koodaimet valitsevat suotimen skannausriviä kohti ja PDF-suodin perii sen, joten ennustimen arvot 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) ja 15 (Optimum) dekoodautuvat kaikki identtisesti: jokaisen rivin johtava tagitavu on se, mitä dekooderin täytyy noudattaa. Asettelun seuraus on yhtä tärkeä kuin semantiikka. Jokainen koodattu rivi on 1 + RowBytes tavua pitkä, syöte ylittää siis tulosteen täsmälleen rivimäärän verran, ja virta, jonka pituus ei ole tasaluku kertaa RowBytes + 1, on määritelmällisesti katkennut. HotPDF tarkistaa tuon rajan ennen kuin se koskettaa ainuttakaan tavua, hylkää minkä tahansa tagin yli 4:n viestillä Invalid PNG predictor row tag, ja lukee edellisen rivin suoraan yhdestä tulostepuskurista sen sijaan, että materialisoisi kaksiulotteisen rivitaulukon. Suotimet 1 ja 3 hakevat taaksepäin BytesPerPixel:n verran nykyisen rivin sisällä, suodin 2 lukee suoraan ylhäältä, suodin 4 ajaa Paeth-valinnan vasemman, ylhäältä ja ylävasemman yli — ja kaikki neljä toimivat jo rekonstruoidun tulosteen päällä, minkä vuoksi ylärivin täytyy olla dekoodattu rivi eikä koskaan suodatettu syöte

uses
  HPDFPredictor;

var
  Filtered, Raster: AnsiString;
  ErrorText: string;
begin
  // /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
  if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
       Int64(1024) * 3 * 8192, Raster, ErrorText) then
    ConsumeRaster(Raster)
  else
    LogStreamDefect('predictor', ErrorText);   // no exception, no partial raster
end;

MaxOutputBytes-argumentti ei ole koristetta. Ennustinvaihe on puretun purkuvaiheen naamio, ja vihamielinen tai vain rikkinäinen /Columns-arvo muuttaa muutaman kilotavun syötteen usean gigatavun varauspyynnöksi. HotPDF laskee bitit riviä kohti, tavut riviä kohti ja rasterin kokonaiskoon ensin Int64-tyypissä, kieltäytyy geometriasta, joka ylivuotaa, ja kunnioittaa kutsujan antamaa kattoa. Anna todellinen raja johdettuna kuvasanakirjasta, ja epäonnistumistila muuttuu kirjatuksi viestiksi asiakaskoneen muistin loppumisdialogin sijaan

Miksi TIFF Predictor 2 turmelee 4-bittiset kuvat?

Koska Predictor 2 on vaakasuoraa erotusta näytettä kohti, ei tavua kohti, ja 1, 2 tai 4 bitillä komponenttia kohti useampi näyte jakaa saman tavun. Yleinen toteutus lisää tavun N-Colors tavuun N, mikä sattuu olemaan oikein 8 bitillä komponenttia kohti ja hiljaa väärin kaikkialla muualla. 8-bittinen RGB-skannaus dekoodautuu täydellisesti, sitten sama koodi tuhoaa 4-bittisen indeksoidun kuvan ensimmäisen kerran, kun sellainen ilmestyy tuotannossa

Oikea aritmetiikka toimii bittikentän sisällä. HotPDF kulkee näytteitä läpi indeksistä Colors indeksiin Colors * Columns - 1, poimii näytteen ja sen saman komponentin vasemman naapurin maskilla (1 shl BitsPerComponent) - 1 sopivassa siirrossa, lisää ne yhteen modulo tuon maskin, ja kirjoittaa tuloksen takaisin häiritsemättä muita samaan tavuun pakattuja näytteitä. Häntäkin on tärkeä: rivi täytetään tavurajaan asti, joten viimeisen näytteen jälkeiset täytebitit täytyy säilyttää koskemattomina sen sijaan, että ne taitettaisiin mukaan aritmetiikkaan. 16 bitillä komponenttia kohti jokainen näyte on big-endian-tavupari, ja yhteenlasku kiertää arvossa $FFFF parin yli sen sijaan, että se siirtäisi ylivuotoa tavujen välillä itsenäisesti; 8 bitillä yksinkertainen tavurekursio on oikein, askeltaen Colors:n verran niin että punainen kertyy punaista vasten ja alfa alfaa vasten. Jokaisessa muunnelmassa rivin ensimmäinen pikseli on kirjaimellinen, ei koskaan erotus, ja rekursio käynnistyy uudelleen jokaisella rivirajalla — TIFF-ennustus ei koskaan lue yläriviä, mikä on koko ero sen ja PNG-perheen välillä

Mitä EarlyChange todella hallitsee LZWDecode:ssa?

Se hallitsee, milloin lukija leventää koodikokoaan yhdellä bitillä, ja yhden koodin verran pois tahdista oleminen turmelee kaiken sen jälkeisen. HotPDF ilmaisee säännön yhtenä invarianttina: sanakirjamerkinnän lisäämisen jälkeen seuraava luku leventyy, kun NextCode saavuttaa arvon (1 shl CodeSize) - Ord(EarlyChange). Kun /EarlyChange 1, ISO 32000-1 §7.4.4:n oletusarvo, vaihto tapahtuu yhtä koodia aikaisemmin; kun /EarlyChange 0, se tapahtuu täsmälleen rajalla. Molempia esiintyy todellisissa tiedostoissa, eikä mikään bittivirrassa kerro, kumpaa koodain käytti. Tilakoneen lopun täytyy liikkua tahdissa: tyhjennyskoodi nollaa koodikoon, bittimaskin, seuraavan vapaan koodin ja lauseen tallennuksen yhdessä, ja tiedon-loppu-koodi luetaan sillä leveydellä, joka on ajankohtainen sillä hetkellä, ei alkuperäisillä 9 bitillä. HotPDF aloittaa arvosta InitialCodeSize 9, rajoittaa koodikoon 12:een ja sanakirjan 4096 merkintään, ja oletusarvoistaa FillOrder:n arvoon foTop, koska PDF pakkaa koodit korkeimman bitin ensin — foBottom on olemassa TIFF-tyylisille virroille, jotka eivät tee niin

uses
  HPDFLZW;

var
  Decoder: TPDFLZWDecompressor;
  Parms: TPDFLZWParms;
  Plain: AnsiString;
begin
  Decoder := TPDFLZWDecompressor.Create;
  try
    Decoder.EarlyChange := True;      // /EarlyChange 1 is the PDF default
    Decoder.FillOrder := foTop;       // high-order bit first
    Decoder.MaxOutputBytes := 256 * 1024 * 1024;
    Decoder.RequireInitialClear := False;
    Decoder.RequireEndOfInformation := False;

    Parms.Predictor := 12;
    Parms.Colors := 3;
    Parms.BitsPerComponent := 8;
    Parms.Columns := 1024;
    Parms.ExpandedTo8Bit := False;
    Parms.ColorSpace := 'DeviceRGB';

    if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
      LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
        Decoder.KwKwKExpansions, Decoder.OutputBytes)
    else
      LogStreamDefect('lzw', Decoder.LastError);
  finally
    Decoder.Free;
  end;
end;

Tilastot ovat olemassa triagea varten, ei turhamaisuutta. Kun tiedosto dekoodautuu oikean pituiseksi mutta vääriksi pikseleiksi, PeakCodeSize ja DictionaryAdds kertovat sinulle heti, leventikö lukija koskaan siellä, missä kirjoittaja levensi. Käännä EarlyChange, dekoodaa uudelleen, vertaa kahta: jos luvut muuttuvat, sinulla on vastauksesi yhdessä ajossa bittilukijan läpikäymisen sijaan

KwKwK-haara, ja milloin virran pitäisi yksinkertaisesti epäonnistua

Ainoa laillinen tapaus, joka näyttää laittomalta, on Code = NextCode, ja HotPDF käsittelee sen rakentamalla merkinnän ennen sen tuottamista. Koodain voi tuottaa koodin lauseelle, jota se määrittelee samassa vaiheessa, mikä tapahtuu aina, kun syöte sisältää muotoa K w K w K olevan kuvion; dekooderi ei voi hakea tuota koodia, koska sitä ei vielä ole olemassa, joten sen täytyy rakentaa Previous + First(Previous), lisätä se uutena merkintänä, ja tuottaa juuri luomansa merkintä. HotPDF laskee nämä muuttujaan KwKwKExpansions ja ristiintarkistaa, että lisäämänsä koodi on koodi, jota siltä pyydettiin. Kaikki NextCode:n yläpuolella on turmelusta, ja siellä dekooderin pitäisi pysähtyä eikä improvisoida: HotPDF nostaa virheen tulevasta koodista, sanakirjan etuliitteestä, joka osoittaa lausearenan ulkopuolelle, täydestä sanakirjasta, ja ensimmäisestä koodista, joka ei ole kirjaimellinen. Kaksi tiukkuuskytkintä ovat tarkoituksella pois päältä oletuksena, RequireInitialClear ja RequireEndOfInformation, koska monet tuotanto-PDF:t jättävät pois johtavan tyhjennyskoodin tai loppuvat dataan ilman päätintä. Kytke ne päälle, kun validoit omaa tuotostasi, jätä ne pois, kun kulutat tiedostoja luonnosta

Missä /DecodeParms todella luetaan ladatun dokumentin puolella

HotPDF ratkaisee /DecodeParms:n tai sen lyhenteen /DP:n kuvavirran sanakirjassa, hyväksyy joko sanakirjan tai taulukon ja ottaa viimeisen alkion, kun se on taulukko, ja kuljettaa sitten Predictor-, Colors-, BitsPerComponent-, Columns- ja EarlyChange-arvot rasteripolkuun. Taulukkotapaus on se, jonka ihmiset unohtavat: virta, joka on suodatettu [/ASCII85Decode /FlateDecode]:lla, kantaa rinnakkaista parametritaulukkoa, ja ennustinasetukset kuuluvat viimeiselle suotimelle, ei ensimmäiselle

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
      for I := 0 to Pdf.GetLoadedImageCount - 1 do
        if Pdf.GetLoadedImageInfo(I, Info) then
        begin
          Bmp := Pdf.ExtractLoadedImage(I);   // nil when the raster is unusable
          if Bmp <> nil then
          try
            Bmp.SaveToFile(Format('image-%d.bmp', [I]));
          finally
            Bmp.Free;
          end;
        end;
  finally
    Pdf.Free;
  end;
end;

Yksi historiallinen vika tuolla polulla kannattaa mainita, koska vikaluokka toistuu. Vanha Flate-parametreineen-rutiini loi purkuvirran ja kopioi sitten alkuperäisestä pakatusta syötteestä, joten ennustinvaihe sai pakattuja tavuja ja purki niitä ahkerasti ennustuksesta: aina väärin, ei koskaan virhettä. Nykyinen koodi lukee vain dekooderista ennen kuin antaa tuloksen jaetulle ennustimelle, ja se hylkää lasketun koon lyhyemmän rasterin sen sijaan, että se palaisi yhä pakattuihin tavuihin — varajärjestely, joka ennen muutti dekoodausvirheen turmeltuneeksi bittikartaksi. Sama ennustintoteutus palvelee nyt myös ristiviittausvirtoja, mikä on hyödyllinen johdonmukaisuus, jos työskentelet myös objektivirtojen ja asteittaisten päivitysten parissa, ja ympäröivä poimintakoneisto käsitellään rinnakkaisartikkelissa ladattujen kuvien ja niiden dekoodaussuodattimien poiminnasta. DCTDecode- tai JPXDecode-muodossa saapuvat kuvat eivät koskaan saavuta ennustinta lainkaan; ne kantavat omaa pakattua pikselimalliaan

Läpäisykyky: yhtenäinen lausearena versus merkintäkohtaiset merkkijonot

Merkintäkohtaisen merkkijonosanakirjan korvaaminen yhtenäisellä lausearenalla mitattiin noin 1,61 kertaa nopeammaksi patologisella syötteellä: 1558 MiB/s versus 969 MiB/s vertailussa, jonka yksittäinen pisin lause ulottuu 7 370 880 tavuun. Syötteen muoto selittää eron, koska klassiset toteutukset valitsevat yhden kahdesta huonosta kompromissista. AnsiString-arvojen sanakirja varaa ja kopioi tuoreen merkkijonon jokaiselle jopa 4096 merkinnälle, jokainen uusi merkintä kopioiden vanhempansa kokonaan; etuliite/loppuliite-pino välttää tuon muistin kokonaan mutta rekonstruoi jokaisen lauseen kulkemalla ketjua taaksepäin tavu kerrallaan ja kääntämällä sen, mikä on kunnossa tavalliselle tekstille ja tuskallista, kun yksi lause ulottuu megatavuihin. HotPDF liittää jokaisen lauseen yhtenäisesti geometrisesti kasvavaan arenaan, indeksoi merkinnät siirtymän ja pituuden mukaan, ja tuottaa lauseen yhdellä Move-kutsulla tulostepuskuriin. Rehellinen kustannus on muisti: arena, joka pitää jokaisen lauseen kokonaisuudessaan, on rajattu kaikkien lauseiden pituuksien summalla eikä merkintämäärällä, mikä on täsmälleen se syy, miksi MaxOutputBytes on olemassa sekä purkajassa että ennustimessa. Johda tuo raja siitä, mitä kuvasanakirja väittää rasterin pitäisi olla, ja valehteleva virta epäonnistuu nopeasti

LZW-purkaja, jaettu ennustin ja tässä esitetty ladatun kuvan poimintapolku toimitetaan osana vakiota HotPDF-komponenttia Delphille ja C++Builderille, täydellisen suodin- ja DecodeParms-viitteen kera tuotesivulla