Tekninen artikkeli

Kuvien purkaminen ladatusta PDF-tiedostosta Delphissä: HotPDF

Sinulla on PDF-tiedosto levyllä, asiakas on skannannut sen laskupinosta, ja tehtäväsi on hakea sivun kuvat takaisin bittikartoiksi OCR-tunnistusta varten. Lataat tiedoston, löydät kuva-XObjectit (image XObjects) ja huomaat sitten sen osan, josta kukaan ei varoita: näiden virtojen tavut eivät ole pikseleitä. Ne ovat JPEG-tietovirtaa, aallokepakattua (wavelet-compressed) JPEG 2000 -dataa, Group 4 -faksiajoa tai Flate-suodatetun paletin takana olevaa indeksoitua rasteria. Kuvaobjekti tietää leveytensä ja korkeutensa, mutta todelliset näytteet (samples) on suljettu sen suodattimen sisään, jonka tiedoston tuottaja on valinnut. Käyttökelpoisen TBitmap-olion saaminen vaatii kyseisen suodattimen purkamista, ja PDF tarjoaa noin kahdeksan erilaista tapaa, joilla tavut voidaan sulkea

Tämän aukon täyttää ExtractLoadedImage HotPDF-kirjastossa, joka on alkuperäinen VCL PDF -komponentti Delphille ja C++Builderille. Se luettelee ladatun asiakirjan kuva-XObjectit, raportoi mitä kukin niistä on ja purkaa ne, jotka pystyy, takaisin 24-bittiseksi bittikartaksi. Mielenkiintoinen osa ei ole API-rajapinta, joka koostuu kolmesta metodista. Se on se, miksi erillisen dekoodauspolun on ylipäätään oltava olemassa ja mitä se voi ja ei voi muuttaa takaisin pikseleiksi

Miksi ladattuja kuvia ei ole valmiiksi dekoodattu

HotPDF:n latausohjelma on rakennettu läpikulun tarkkuuden (pass-through fidelity) ympärille. Kun kutsut LoadFromFile-metodia, kuvavirrat säilytetään täsmälleen sellaisina kuin ne esiintyvät lähdetiedostossa: alkuperäinen suodatin, alkuperäiset pakatut tavut ja alkuperäinen sanakirja. Tämä on tarkoituksellista. Asiakirjan lataamisen koko ydin on yleensä sivujen kopiointi, tiedostojen yhdistäminen, leimaaminen, oikeuksien muuttaminen ja tiedoston uudelleenkirjoittaminen. Kaikessa tässä halvin ja turvallisin tapa on jättää jokainen kuvavirta koskemattomaksi. Jokaisen kuvan dekoodaaminen rasteriksi latauksen yhteydessä kuluttaisi muistia ja suoritinaikaa työhön, jota useimmat kutsujat eivät koskaan tarvitse, ja uudelleenkoodaus tallennuksen yhteydessä heikentäisi kuvia, jotka olisi pitänyt kopioida sellaisenaan

Tämän seurauksena ladattu objektikaavio ei sisällä pikseleitä. Kuva-XObject, jonka /Filter on /DCTDecode, sisältää JPEG-tavuja; HotPDF ei koskaan suorittanut JPEG-dekoodausta sitä vastaan, koska kopioi-ja-uudelleenkirjoita-polulla mikään ei sitä vaatinut. Joten kun todella haluat pikseleitä, purku-API:n on tehtävä dekoodaus itse alusta alkaen mille tahansa suodattimelle, jota kyseinen kuva käyttää. Tämä on sama syy, miksi koodauspuolen koodekit ovat itsenäisiä latausohjelmasta: artikkeli JPEG 2000 -kuvien lisäämisestä PDF-tiedostoihin Delphissä kuvaa, miten JPX-moottori kytkeytyy luontipuolelle, eikä kyseistä moottoria yksinkertaisesti oltu kytketty lukupolulle ennen kuin purku-API sitä vaati

Kolmen metodin rajapinta

Rajapinta on pieni. GetLoadedImageCount palauttaa, kuinka monta kuva-XObjectia ladattu asiakirja sisältää. GetLoadedImageInfo täyttää kuvaustietueen (descriptor record) yhdelle niistä indeksin perusteella. ExtractLoadedImage palauttaa puretun bittikartan, tai arvon nil, jos kuvaa ei voida dekoodata. Luettelo perustuu indekseihin ja on vakaa tietylle lataukselle: sisäisesti se käy läpi epäsuorien objektien taulukon ja kerää jokaisen virran, jonka /Subtype ratkeaa arvoksi /Image, joten indeksi, jonka välität metodille GetLoadedImageInfo, on sama kuin jonka välität metodille ExtractLoadedImage

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I, Count: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-invoices.pdf', '') <= 0 then
      Exit;
    Count := Pdf.GetLoadedImageCount;
    for I := 0 to Count - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if not Info.Decodable then
        Continue;                       // filter or colour space not supported
      Bmp := Pdf.ExtractLoadedImage(I);
      if Bmp <> nil then
      try
        Bmp.SaveToFile(Format('img_%d.bmp', [I]));
      finally
        Bmp.Free;                       // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Tässä on kaksi tärkeää sopimuksellista yksityiskohtaa. Ensinnäkin palautettu TBitmap on sinun vapautettavissasi; asiakirja ei välimuistita tai omista sitä. Toiseksi, tarkista Decodable ennen kutsua, ja tarkista tulos arvoa nil vastaan sen jälkeen. Metodi ei heitä poikkeusta, jos suodatinta ei tueta, vaan se palauttaa nil-arvon. Hiljainen nil-tulos eräajossa on juuri sellainen asia, joka voi niellä kokonaisen sivun tuhansien sivujen työstä ilman, että kukaan huomaa mitään

Kuvaajan (descriptor) lukeminen ennen dekoodausta

THPDFLoadedImageInfo kertoo, mikä kuva on ilman sitoutumista täyteen dekoodaukseen. Sen kentät tulevat suoraan kuvasanakirjasta: Width ja Height näytteinä (samples), BitsPerComponent, ColorComponents ja ColorSpace kuvaamassa dekoodauksen jälkeistä tulkintaa (1 harmaasävylle, 3 RGB:lle, 4 CMYK:lle), Filter nimettynä pakkauksena, IsImageMask sapluunamaskeille (stencil masks), ObjectNumber taustalla olevalle epäsuoralle objektille, ja Decodable

Tämä viimeinen lippu on se rehellinen. Decodable on True vain silloin, kun käynnissä oleva versio pystyy todella muuttamaan tämän kyseisen suodatin- ja väriavaruusyhdistelmän bittikartaksi. Se koodaa todellisen tukimatriisin, ei toivetta: kuva, jonka suodatinta nykyinen versio ei ymmärrä, raportoi Decodable = False, ja voit haarantua tämän mukaan lokitukseen, ohitukseen tai siirtyä purkamaan raakaa virtaa itse. Käsittele sitä esiehtona, älä vihjeenä

// Triage every image before committing to a decode.
var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  I: Integer;
begin
  // ... Pdf loaded ...
  for I := 0 to Pdf.GetLoadedImageCount - 1 do
  begin
    if not Pdf.GetLoadedImageInfo(I, Info) then
      Continue;
    if Info.Decodable then
      // ExtractLoadedImage(I) will return a TBitmap
    else
      // unsupported filter/colour space: log the object and skip
      Writeln(Format('Image %d obj %d: %dx%d %s/%s not decodable',
        [I, Info.ObjectNumber, Info.Width, Info.Height,
         String(Info.Filter), String(Info.ColorSpace)]));
  end;
end;

Yksi toteutuksen yksityiskohta iskee ihmisiin, jotka rakentavat kuvaajatietueita käsin. THPDFLoadedImageInfo sisältää kaksi AnsiString-kenttää, Filter ja ColorSpace. Nämä ovat hallittuja tyyppejä viitelaskennalla (reference counting), joten refleksinomainen tietueen nollaus FillChar(Info, SizeOf(Info), 0) on tässä väärin: se kirjoittaa merkkijonoviitteen yli vähentämättä sen viitelaskuria, mikä johtaa muistivuotoon tai korruptioon. HotPDF alustaa tietueen kenttä kentältä juuri tästä syystä, ja jos koskaan kopioit tämän kaavan omaan koodiisi, tee samoin

Yksi jakelija, kahdeksan suodatinpolkua

Syy siihen, miksi tämä ominaisuus vaati useita julkaisuja yhden sijaan, on se, ettei PDF:ssä ole kuvaformaattia. Siinä on suodattimia, ja standardin ISO 32000-1 kohdan §8.9.5 mukaan kuva-XObject voi nimetä minkä tahansa niistä /Filter-avaimessa, ja näytteiden tulkintaa hallitaan erikseen avaimilla /ColorSpace, /BitsPerComponent ja valinnaisella /Decode-taulukolla. ExtractLoadedImage lukee suodattimen nimen ja ohjaa kullekin tapaukselle oman dekooderin. Tuettu sarja, joka rakennettiin versioiden v2.229 ja v2.231 välillä, kattaa nykyään kahdeksan erillistä polkua

  • Raakarasterit (FlateDecode, LZWDecode tai ilman suodatinta) 8-bittisessä DeviceRGB- tai DeviceGray-muodossa. Tavut puretaan pakatuksi rasteriksi, ja ainoa muunnos on kanavien vaihto, jota käsitellään jäljempänä
  • DCTDecode (JPEG). Tietovirta toimitetaan VCL:n TJPEGImage-luokalle, joka selvittää geometrian ja värit, ja tulos sijoitetaan 24-bittiseen bittikarttaan
  • JPXDecode (JPEG 2000). Dekoodataan OpenJPEG-taustamoottorin kautta, joka on sama moottori kuin JPEG 2000 -artikkelissa kuvattu. Suuren bittisyvyyden komponentit skaalataan alas 8 bittiin
  • Indeksoitu väri. Paletti luetaan [/Indexed base hival lookup] -taulukosta ja jokainen näyte laajennetaan hakutaulukon kautta todelliseksi väriksi
  • DeviceCMYK. Nelikanavaiset näytteet muunnetaan RGB-muotoon vakiomuotoisella muste-valkoiselle-kaavalla
  • Alle 8-bittinen DeviceGray ja indeksoitu arvoilla 1, 2 tai 4 bittiä per komponentti, puretaan näyte näytteeltä ja skaalataan alueelle 0–255
  • CCITTFaxDecode, Group 3- ja Group 4 -faksisuodattimet, jotka dekoodataan omalla T.4/T.6-taustamoottorilla
  • JBIG2Decode, korkean pakkaussuhteen kaksitasoinen (bilevel) suodatin, joka dekoodataan rekisteröidyllä JBIG2-taustamoottoril, jota käsitellään koodauspuolelta artikkelissa JBIG2-kaksitasopakattujen kuvien tekeminen Delphissä

Kaikki päätyvät samaan paikkaan: 24-bittiseen BGR-bittikarttaan, koska sitä VCL:n TBitmap käyttää sisäisesti ja sitä kaikki jatkokäyttäjät odottavat

Muunnokset, jotka muuttavat pikseleitä hiljaisesti

Kahdessa näistä poluista käytetään muunnosta, joka on helppo tehdä hienovaraisesti väärin ja joka on syytä ymmärtää, vaikket koskaan koskisi dekooderiin itse. Ensimmäinen on värijärjestyksen vaihto. PDF:n DeviceRGB-rasteri tallentaa näytteet punainen-vihreä-sininen-järjestyksessä, ylin rivi ensin. VCL:n 24-bittinen ScanLine tallentaa ne sininen-vihreä-punainen-järjestyksessä. Joten tavallisen RGB-kuvan dekoodaaminen ei ole pelkkää muistikopiointia (memcpy); jokaisen pikselin ensimmäinen ja kolmas tavu vaihdetaan keskenään matkalla ScanLineen. Tee tämä väärinpäin, ja punaiset sekä siniset värisi vaihtavat paikkaa, mikä näyttää hyvältä harmaasävyisessä testikuvassa, mutta katastrofaalisen väärältä värikuvassa. Rivijärjestys kartoittuu onneksi suoraan: PDF:n ylhäältä alaspäin suuntautuvat rasterit vastaavat VCL:n ScanLine[0]-riviä ylimpänä visuaalisena rivinä, joten pystysuoraa kääntöä ei tarvita

Toinen on CMYK. PDF:n DeviceCMYK-kuvat sisältävät neljä mustetta, ja muunnos RGB-muotoon on kanavakohtainen laskutoimitus, ei haku: jokainen ulostulokanava on (255 - muste) * (255 - K) / 255. Tämä on laitekohtainen approksimaatio, ei värihallittu muunnos ICC-profiilin kautta, joten tulos on riittävän tarkka näyttöä ja uudelleenrasterointia varten, mutta se on väärä polku, jos tarvitset painotarkkaa väriä. Jos työnkulkusi vaatii ehdotonta tarkkuutta, käsittele purettua bittikarttaa esikatselukuvana ja säilytä alkuperäinen CMYK-virta värihallittua putkistoa varten

Indeksoitu polku piilottaa oman jäsennyssudenkuoppansa. Paletti /Indexed-väriavaruudessa voidaan tallentaa kirjaimellisena merkkijonona (literal string) tai heksadesimaalimerkkijonona (hexadecimal string), ja HotPDF tallentaa heksamerkkijonon arvon heksatekstinä, ei purettuina tavuina. Joten kun paletti on heksamerkkijono, hakutaulukko on ensin ajettava heksa-tavuiksi-dekoodauksen läpi; kirjaimellinen merkkijono on jo valmiiksi raakatavuina. Jos ohitat tämän haaran, nelivärinen indeksoitu kuva tulee ulos roskana, koska jokainen palettimerkintä luetaan väärältä tavurajalta

Suodatinketjut: viimeinen suodatin on kuvan oma

Yksittäinen /Filter-nimi on helppo tapaus. PDF sallii myös suodatinketjun (chain of filters), jossa tietovirta on ajettu useamman suodattimen läpi peräkkäin, lueteltuna järjestyksessä /Filter-taulukossa, kuten [/ASCII85Decode /FlateDecode] tai [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). Semantiikka on tarkka: suodattimet soveltuvat koodauksessa vasemmalta oikealle, joten dekoodauksessa ne puretaan oikealta vasemmalle, ja taulukon viimeinen suodatin on se, joka todellisuudessa määrittää kuvaformaatin. Alussa olevat suodattimet ovat vain sen ympärille käärittyjä siirtokoodauksia (transport encodings)

Purkaja hoitaa tämän kuorimalla (peeling). Ennen kuin mikään kuvadekooderi suoritetaan, ketjun jokaista suodatinta paitsi viimeistä sovelletaan tuottamaan syöte, jota viimeinen suodatin odottaa, ja vasta sitten tapahtuu ohjaus kyseiselle viimeiselle suodattimelle. Joten ketju [/ASCII85Decode /DCTDecode] poistaa ensin ASCII85-koodauksen virrasta ja ohjaa tuloksen JPEG-polulle; raa'an rasterin ympärille kääritty [/FlateDecode] purkaa pakkauksen (inflates) ja ajaa rasteripolun. Tämän ansiosta kahdeksan dekooderia voivat pysyä yksinkertaisina. Yhdenkään niistä ei tarvitse tietää ASCII85- tai heksasiirtokääreistä, koska siinä vaiheessa kun dekooderi näkee tavut, kääreet ovat jo poissa. Se tarkoittaa myös sitä, että ketju, jonka viimeistä suodatinta ei tueta, epäonnistuu siististi jo ohjausvaiheessa eikä puolivälissä suoritusta

Mihin purkaminen päättyy, ja mitä tehdä sitten

Ole rehellinen itsellesi rajojen suhteen. Kuva, jonka viimeinen suodatin on tuetun joukon ulkopuolella, palauttaa arvon nil, ja samoin tekee kuva, jonka väriavaruutta versio ei osaa tulkita. Pehmeitä maskeja (soft masks) ja alfa-kanavia ei rekonstruoida bittikarttaan; saat peruskuvan, et yhdistettyä (composited) lopputulosta. JPEG 2000 -muodon yli 8 bitin bittisyvyydet skaalataan alaspäin, mikä on tarkoituksella häviöllistä ja väärä valinta, jos olet arkistoimassa uudelleen näyttämisen sijaan. Ja kuvamaski (image mask) – yksibittinen sapluuna ilman omaa väriään – kuvataan kuvaajassa, mutta se on täysin eri asia kuin kuvallinen kuva; jos dekoodaat sen odottaen valokuvaa, tulet yllättymään

Kun purkaminen ei riitä, raakavirta on edelleen saatavilla ladatussa objektikaaviossa suodattimineen kaikkineen, ja voit vetää sen ulos tavu tavulta ja toimittaa sen omalle erikoistuneelle koodekillesi. Tämä on se varasuunnitelma, jonka läpikulkusuunnittelu (pass-through design) säilyttää tarkoituksella: alkuperäisiä tavuja ei koskaan heitetä pois, joten pahimmassa tapauksessa dekoodaat ne itse sen sijaan, että tieto olisi kadonnut. Useimmissa todellisissa tehtävissä tuetut kahdeksan suodatinta kattavat kuitenkin sen, mitä skannerit, toimisto-ohjelmistot ja raporttimoottorit todellisuudessa tuottavat, ja silmukka GetLoadedImageCount-metodin ja Decodable-tarkistuksen yli muuttaa ladatun PDF-tiedoston bittikarttakansioksi vain muutamalla rivillä

Tässä kuvattu ladattujen kuvien purku-API sekä täydellinen suodatinvalikoima toimitetaan HotPDF-komponentti-tuotteen mukana Delphille ja C++Builderille