Tekninen artikkeli

PDF-sivujen renderöinti 1-bittiseksi mustavalkoiseksi Delphissä

Faksiyhdyskäytävä ei halua 24-bittistä sivukuvaasi. Sitä ei halua myöskään arkistointiputki, joka tallentaa miljoona skannatun näköistä laskua, eikä OCR-etupää, joka muuttaa kaiken mustavalkoiseksi ennen kuin se alkaa etsiä merkkejä. Kaikki kolme haluavat saman asian: siistin 1-bittisen bittikartan, yksi bitti per pikseli, jossa jokainen piste on joko mustetta tai paperia. Jos annat niille värikuvan (BMP), ne heittävät joka tapauksessa pois 23 bittiä per pikseli, yleensä huonommalla rasterointikierroksella kuin mitä olisit itse voinut tehdä. Mielenkiintoinen kysymys kuuluu, missä vaiheessa tämä värien vähentäminen tulisi tehdä, ja vastaus PDFlibPas-kirjastossa kertoo jotain hyödyllistä siitä, miten voit laajentaa renderöijää, jota et halua kirjoittaa kokonaan uudelleen

PDFlibPas on alkuperäinen Object Pascal -pohjainen PDF-kirjasto Delphille ja C++Builderille. Sen renderöintiydin rasteroi sivun bittikartaksi ja voi tuottaa BMP-, PNG-, JPEG-, WMF- ja muutamia muita tiedostomuotoja. Mitä se ei kuitenkaan tehnyt ennen viime aikoja, oli todellisen mustavalkoisen bittikartan palauttaminen tai sivun vain osittainen renderöinti. Molemmat ominaisuudet tulivat versiossa v3.83.0, ja molemmat rakennettiin ohuiksi mukavuuskerroksiksi olemassa olevan renderöijän päälle sen sijaan, että itse rasteroijaa olisi muutettu. Tämä rajoitus on koko tarinan ydin

Miksi värit vähennetään vasta renderöinnin jälkeen eikä sen sisällä

Ilmeinen tapa tuottaa 1-bittinen kuva on käskeä rasteroijaa piirtämään 1-bittisenä. Se on kuitenkin tapa, joka rikkoo kaiken muun. Renderöijän sisäinen bittikartta luodaan kovakoodatulla arvolla PixelFormat := pf24bit PDFlibRenderer-muodostimessa (constructor), ja tämän 24-bittisen pinnan jakavat kaikki renderöintipolut: PNG-vienti, laiteyhteys-esikatselu (device context preview), JPEG-tulostus ja kaikki muu. Jos muutat sen pf1bit-muotoon suoraan lähteessä, et ole lisännyt mustavalko-ominaisuutta, vaan olet heikentänyt väritarkkuutta kirjaston jokaiselta käyttäjältä ja sitoutunut selvittämään kymmenen jatkovirhettä

Siksi RenderPageToMonochromeFile valitsee päinvastaisen tien. Se renderöi sivun normaalisti väliaikaiseksi 24-bittiseksi BMP-kuvaksi ja tiivistää sen vasta sitten 1-bittiseksi jälkikäsittelyvaiheessa. Renderöijään ei kosketa. Mustavalkoinen toiminta elää kokonaan mukavuusmetodissa, mikä tarkoittaa, ettei se voi vaikuttaa keneenkään, joka ei kutsu sitä. Tämä on sellainen kompromissi, joka kannattaa mainita selvästi: jälkikäsittely maksaa yhden ylimääräisen bittikartan varauksen ja väliaikaistiedoston, mutta vastineeksi se pitää kuormaa kantavan ytimen täysin koskemattomana. Ominaisuudelle, joka palvelee faksi- ja arkistointierikoistapauksia, tämä on oikea valinta

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI is the classic Group 4 fax resolution; page index is 1-based
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Miten 1-bittiseksi muuntaminen todella tapahtuu

Värien vähentäminen nojaa GDI:hin eikä käsin tehtyyn kynnysarvosilmukkaan, ja tällä valinnalla on merkitystä tulosteen laadulle. Metodin sisällä 24-bittinen väliaikainen bittikartta ladataan TBitmap-luokkaan, toinen samankokoinen TBitmap luodaan arvolla PixelFormat := pf1bit, ja pikselit siirretään yhdellä blit-kutsulla:

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

Temppu on SetStretchBltMode arvolla HALFTONE. Vaikka lähde ja kohde ovat samankokoisia, joten skaalausta ei tapahdu, venytystila määrää silti sen, miten GDI kartoittaa värit 1-bittiseen palettiin. HALFTONE saa sen soveltamaan rasterointiditherointia (halftone dithering), muuttaen harmaat alueet ja pehmennetyt tekstin reunat mustien ja valkoisten pisteiden kuvioiksi sen sijaan, että ne leikattaisiin jyrkästi toiseen kahdesta väristä. Jos jätät tilakutsun pois tai käytät oletusasetusta BLACKONWHITE, harmaasävyinen sisältö muuttuu palikkamaiseksi kynnysarvokuvioksi. Skannattujen dokumenttien ja OCR-esikäsittelyn kohdalla rasteroitu lopputulos on lähes aina se, mitä halutaan

Yksi yksityiskohta on ehdoton ja helppo tehdä väärin: väliaikaisen renderöinnin on oltava BMP. RenderPageToMonochromeFile kutsuu yleistä renderöijää optio-koodilla 0, joka on BMP. RenderPageToFile-metodin optio-argumentti on pieni kokonaisluku-enum, eivätkä sen arvot ole tässä yhteydessä vaihdettavissa keskenään: 0 on BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG ja niin edelleen. Värienvähentäjä tekee sitten TBitmap.LoadFromStream-kutsun väliaikaistiedostolle. Jos sille syötetään WMF välittämällä arvo 2, lataus heittää poikkeuksen "Bitmap image is not valid", koska Windows Metafile on vektoritietovirta, ei DIB-bittikartta. Mustavalkomuunnos on rasterioperaatio alusta loppuun, joten välivaiheen on oltava rasterimuodossa

Sivun osa-alueen renderöinti

Toinen metodi, RenderPageRegionToFile, renderöi vain suorakulmion alueen sivusta koko sivun sijaan. Käyttötapaukset ovat tuttuja kaikille, jotka ovat rakentaneet dokumenttikatselulaitteita: allekirjoituslohkon rajaaminen sopimuksesta, laatan (tile) luominen suuresta piirustuksesta zoomattua karttaa varten, tai leimatun alueen hakeminen pikkukuvaa varten ilman, että koko sivua tarvitsee rasteroida korkealla DPI-tarkkuudella. Allekirjoitus on suoraviivainen:

// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

Rajausparametri, joka ei tehnyt mitään

RenderPageToDCClip oli sisältänyt Clip-parametrin jo pitkään, ja se oli valetta. Kutsu hyväksyi argumentin, välitti sen alas aliohjelmalle TPDFPageTree.RenderPageToDC, mutta kyseinen toteutus ohitti sen kokonaan eikä koskaan toimittanut sitä renderöijälle. Voit välittää minkä tahansa suorakulmion ja sait koko sivun takaisin. Jokainen, joka oli kytkenyt RenderPageToDCClip-kutsun odottaen rajausta, sai koko sivun renderöinnin eikä kenties edes huomannut sitä riippuen omasta asettelustaan

Versio v3.83.0 kytki johdot. RenderPageToDC jäsentää nyt saman "Left,Top,Width,Height"-pistekolmion ja soveltaa sitä todellisena GDI-rajausalueena (clip region) kohdelaitteen yhteyteen (device context) ennen kuin renderöijä piirtää. Muunnos pisteistä laitepikseleiksi on tavallinen DPI / 72 -skaalaustekijä, jota sovelletaan kaikkiin neljään reunaan. Renderöintiä ympäröivä sekvenssi on standardi tallenna/rajaa/palauta-tanssi:

// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);

SaveDC / RestoreDC(-1) -pari pitää tämän turvallisena kutsua toistuvasti: rajausalue työnnetään DC:n tilapinoon, sivu piirretään, ja alkuperäinen rajaus palautetaan riippumatta siitä, miten renderöinti päättyy. RestoreDC(TargetDC, -1) palauttaa viimeisimmäksi tallennetun tilan, mikä on standardi tapa tasapainotettuun tallennukseen ja palautukseen. Jos palautus ohitetaan, samaa laiteyhteyttä (DC) seuraavaan koko sivun renderöintiin käyttävä kutsuja huomaa sen olevan mystisesti rajattu edellisen alueen mukaan. Kuolleen parametrin korjaaminen korjasi samalla myös RenderPageRegionToFile-metodin ilmaiseksi, koska tämä uusi metodi kulkee juuri kyseisen polun kautta

Yksi opittava toiminnallinen seikka: rajaus leikkaa (crops), se ei skaalaa. Sivu rasteroidaan edelleen pyytämälläsi DPI-tarkkuudella sen normaalissa asennossa, ja rajausalue yksinkertaisesti heittää kaiken suorakulmion ulkopuolisen pois. Et ole zoomaamassa aluetta täyttämään tulostetta; olet leikkaamassa ikkunaa täyden resoluution renderöinnistä. Jos haluat suurentaa aluetta, nosta DPI-arvoa. Suorakulmion koordinaatit tulkitaan laitetilassa pisteistä pikseleiksi -skaalauksen jälkeen, mitattuna renderöidyn pinnan vasemmasta yläkulmasta, joten suunnittele Left ja Top sivun yläreunasta alaspäin. Jos haluat syventyä siihen, miten PDFlibPas ajaa laiteyhteyttä näytölle, rinnakkaisartikkeli tulostuksen esikatselusta ja laiteyhteystulostuksesta käy läpi saman DC-putkiston näyttöpuolelta

Rehellinen raja: 1-bittinen BMP, ei G4 TIFF

Tätä olisi helppo markkinoida liikaa "faksivalmiina tulosteena", joten tässä on raja sanottuna suoraan. RenderPageToMonochromeFile tuottaa pf1bit BMP-kuvan. Se ei tuota CCITT Group 4 TIFF -tiedostoa, jota todellinen faksityönkulku tai TIFF-arkisto yleensä odottaa. Syy on käytännöllinen eikä unohdus: PDFlibPas-kirjaston CCITT-yksikkö purkaa tällä hetkellä G4-virtoja, mutta siinä ei ole G4-enkooderia. Ilman enkooderia ei ole paikkaa, johon kirjoittaa pakattuja mustavalkoisia juoksuja (compressed monochrome runs), joten mustavalkoinen polku pysähtyy pakkaamattomaan 1-bittiseen DIB-bittikarttaan

Käytännössä tämä on silti hyödyllistä. 1-bittinen BMP on oikea pikselimuoto, rasteroitu ja valmis, ja useimmat faksi-, arkisto- tai OCR-työkaluketjut ottavat sen mielellään vastaan tai muuntavat sen itse G4-muotoon yhdellä jatkovaiheella. Mutta jos vaatimuksesi on kirjaimellisesti Group 4 TIFF suoraan kirjastosta, tämä ei ole vielä sitä, ja sinun tulee suunnitella oma pakkausvaiheesi. Se, että tietää, mihin ominaisuus pysähtyy, on yhtä arvokasta kuin se, että tietää, mitä se tekee

Molemmat metodit ovat tietoisen pieniä, ja se on tästä sivusta mukaan otettava suunnitteluoppitunti: renderöijän päällä istuva mukavuus-API voi tuoda todellista kyvykkyyttä, kuten mustavalkotulostuksen ja alueen rajauksen, koskematta itse rasteroijaan ja horjuttamatta muita kutsujia. Kun sinun on valittava renderöintimoottorien välillä taustalla olevaa rasterointia varten, yleiskatsaus monimoottorisesta PDF-renderöinnistä Delphissä käsittelee kompromisseja syvällisesti. Nähdäksesi koko renderöintipinnan ja muun API:n, PDFlibPas Delphi PDF -kirjaston tuotesivu tarjoaa täydellisen kuvan