Műszaki cikk

PDF-oldalak leképezése 1 bites monokróm formátumba Delphiben

Egy fax-átjárónak (fax gateway) nincs szüksége a 24 bites oldalleképezésre. Ugyanez igaz az archiválási munkafolyamatra is, amely egymillió szkenneltnek tűnő számlát tárol, valamint az OCR előtétprogramra (OCR front-end), amely mindent fekete-fehérre küszöböl ki (thresholds), mielőtt még karaktereket keresne. Mindhárom ugyanazt akarja: egy tiszta 1 bites bitképet, képpontonként egy bittel, ahol minden pont vagy tinta, vagy papír. Ha teljes színű BMP-t adunk át nekik, akkor is eldobnak képpontonként 23 bitet, általában rosszabb dithering (szórási) eljárással, mint amit mi magunk elvégezhettünk volna. Az érdekes kérdés az, hogy hol történjen ez a lefelé konvertálás, és a PDFlibPas-ban adott válasz valami hasznosat mond el arról, hogyan bővíthetünk egy olyan leképezőt (renderer), amelyet nem szeretnénk újraírni

A PDFlibPas egy natív Object Pascal PDF-könyvtár Delphihez és C++Builderhez. Leképezési magja (rendering core) bitképpé raszterez egy oldalt, és képes BMP, PNG, JPEG, WMF és néhány más formátum kiadására. Amit egészen a közelmúltig nem tudott, az a valódi monokróm bitkép visszaadása, illetve egy oldalnak csak egy részének leképezése. Mindkét funkció a v3.83.0 verzióban jelent meg, és mindkettő vékony kényelmi rétegként épült fel a meglévő leképező tetején, ahelyett, hogy magát a raszterezőt (rasterizer) módosították volna. Ez a korlátozás jelenti a teljes történetet

Miért a leképezés után végezzük a lefelé konvertálást, nem pedig a leképezőn keleten

Az 1 bites kép előállításának nyilvánvaló módja az lenne, ha megmondanánk a raszterezőnek, hogy 1 bites formátumban rajzoljon. Ez azonban az a módszer is, amely minden mást elrontana. A leképező belső bitképe a PDFlibRenderer konstruktorában kódolt PixelFormat := pf24bit értékkel jön létre, és ezen a 24 bites felületen osztozik minden leképezési útvonal: a PNG-export, az eszköz-kontextus (device context) előnézet, a JPEG-kimenet és minden más. Ha ezt a forrásnál pf1bit-re váltjuk, akkor nem egy monokróm funkciót adunk hozzá, hanem rontjuk a színhűséget a könyvtár minden hívója számára, és egy tucatnyi kimeneti regresszió hibakeresését vállaljuk magunkra

Egy fax-átjárónak (fax gateway) nincs szüksége a 24 bites oldalleképezésre. Így a RenderPageToMonochromeFile az ellenkező utat választja. Normálisan leképezi az oldalt egy ideiglenes 24 bites BMP-be, és csak ezután csökkenti azt 1 bitesre utófeldolgozási lépésként. A leképező érintetlen marad. A monokróm viselkedés teljesen a kényelmi metódusban él, ami azt jelenti, hogy nem érinthet senkit, aki nem hívja meg. Ezt a fajta kompromisszumot érdemes kifejezetten megnevezni: egy utófeldolgozás egy extra bitkép-lefoglalást és egy ideiglenes fájlt igényel, cserébe viszont teljesen kívül tartja a kritikus fontosságú magot a módosításokon. Egy olyan funkciónál, amely a fax- és archiválási határeseteket szolgálja ki, ez a mérleg megfelelő oldala

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // A 200 DPI a klasszikus Group 4 fax felbontás; az oldalinformáció 1-alapú
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Hogyan történik valójában az 1 bites tömörítés

A lefelé konvertálás a GDI-re támaszkodik egy kézzel írt küszöbérték-ciklus (threshold loop) helyett, és ennek a döntésnek nagy jelentősége van a kimeneti minőség szempontjából. A metóduson belül az ideiglenes 24 bites bitkép betöltődik egy TBitmap-be, létrejön egy második TBitmap PixelFormat := pf1bit formátumban azonos méretekkel, és a képpontok egyetlen másolással (blit) kerülnek átvitelre:

// a RenderPageToMonochromeFile metóduson belül, a 24 bites ColorBmp betöltése után
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// A HALFTONE arra utasítja a GDI-t, hogy a 24 bites forrást szórja le 1 bitesre
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');

A trükk a SetStretchBltMode használata a HALFTONE értékkel. Bár a forrás és a cél mérete megegyezik, így méretezés nem történik, a nyújtási mód továbbra is szabályozza, hogyan képezi le a GDI a színeket az 1 bites palettára. A HALFTONE féltónusos szórás (halftone dithering) alkalmazását kényszeríti ki, így a szürke területek és az élsimított szövegszélek fekete és fehér pontok mintázatává válnak ahelyett, hogy durván a két szín egyikére lennének levágva. Ha elhagyjuk a mód beállítását, vagy az alapértelmezett BLACKONWHITE módot használjuk, a szürkeárnyalatos tartalom blokkos, küszöbölt formákká esik szét (posterizes). Szkennelt dokumentumok és OCR előfeldolgozási kimenetek esetében szinte mindig a szórt (dithered) eredmény a kívánatos

Egy részlet nem vitás és könnyen elrontható: az ideiglenes leképezésnek BMP-nek kell lennie. A RenderPageToMonochromeFile a 0 opciókóddal hívja meg az általános leképezőt, ami a BMP-t jelenti. A RenderPageToFile metódus options argumentuma egy kis egész számokból álló felsorolás (enum), és az értékek nem cserélhetők fel erre a célra: a 0 a BMP, az 1 a JPEG, a 2 a WMF, a 3 az EMF, az 5 a PNG és így tovább. A lefelé konvertáló ezután végrehajtja a TBitmap.LoadFromStream-et az ideiglenes fájlon. Ha WMF-et adunk át neki a 2 átadásával, ez a betöltés a „Bitmap image is not valid” hibát dobja, mivel a Windows Metafájl egy vektoros rekordfolyam, nem pedig DIB (Device-Independent Bitmap). A monokróm lefelé konvertálás egy teljes mértékben raszteres művelet, így a köztes fájlnak raszteres formátumnak kell lennie

Egy oldalnak csak egy részrégiójának a leképezése

A második metódus, a RenderPageRegionToFile az egész oldal helyett csak az oldal egy téglalapját képezi le. A használati esetek ismerősek, ha építettünk már dokumentumnézőt: aláírás-blokk kivágása egy szerződésből, egy csempe generálása egy nagy rajz nagyított térképéhez, vagy egy bélyegzett terület lekérése egy miniatűrhöz (thumbnail) anélkül, hogy az egész oldalt raszterezni kellene magas DPI mellett. A metódus hívása egyszerű:

// A Clip formátuma „Bal,Fent,Szélesség,Magasság” PDF pontokban (72 pt = 1 hüvelyk)
// Itt: egy 2,5 hüvelyk x 1 hüvelyk méretű doboz, egy hüvelykre az oldal bal felső sarkától
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

A vágási karakterlánc négy vesszővel elválasztott double érték PDF pontokban kifejezve, amelyet a metóduson belül manuálisan értelmez a rendszer a területi beállítások (locale) és a DelimitedText furcsaságainak elkerülése érdekében. A szélességből és magasságból a metódus kiszámítja a kimeneti bitkép méretét Round(Width * DPI / 72) és Round(Height * DPI / 72) formában, lefoglal egy pontosan ekkora memóriában lévő pf24bit bitképet, és leképezi azt a saját eszköz-kontextusába a RenderPageToDCClip-en keresztül. Az eredményfájl csak a kivágott téglalapot tartalmazza, a régióra méretezve a teljes oldal helyett

A semmit sem csináló clip paraméter

Itt a munka finomabb volt, mint amilyennek látszik. A RenderPageToDCClip már régóta hordozott egy Clip paramétert, és ez hazugság volt. A hívás elfogadta az argumentumot, továbbadta a TPDFPageTree.RenderPageToDC-nek, és az a megvalósítás teljesen figyelmen kívül hagyta, soha nem adta át a leképezőnek. Bármilyen téglalapot átadhatott, és visszakapta a teljes oldalt. Bárki, aki a RenderPageToDCClip-et a kivágás elvárásával kötötte be, egy teljes oldalas leképezést kapott, és az elrendezéstől függően talán észre sem vette

A v3.83.0 összekötötte a szálakat. A RenderPageToDC most már értelmezi ugyanazt a „Bal,Fent,Szélesség,Magasság” pont-téglalapot, és valós GDI vágási régióként (clip region) alkalmazza a cél eszköz-kontextuson, mielőtt a leképező rajzolna. A pontokról eszköz-képpontokra történő átszámítás a szokásos DPI / 72 skálatényező, mind a négy élre alkalmazva. A leképezés körüli sorozat a szabványos mentés/vágás/visszaállítás tánc:

// a TPDFPageTree.RenderPageToDC metóduson belül, ha a Clip nem üres
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... a leképező ide rajzolja az oldalt ...
// a finally blokkban:
RestoreDC(TargetDC, -1);

A SaveDC / RestoreDC(-1) pár teszi ezt biztonságossá az ismételt hívásokhoz: a vágási régió rákerül a DC állapotvermére, az oldal kirajzolódik, és az eredeti vágás visszakerül a helyére, függetlenül attól, hogyan lép ki a leképezés. A RestoreDC(TargetDC, -1) visszaállítja a legutóbb elmentett állapotot, ami a kiegyensúlyozott mentés/visszaállítás standard kifejezése (idiom). Ha kihagyja a visszaállítást, egy olyan hívó, amely ugyanazt a DC-t használja fel egy későbbi teljes oldalas leképezéshez, rejtélyes módon az utolsó régióra vágva találja azt. A halott paraméter javítása a RenderPageRegionToFile metódust is ingyen javította, mivel ez az új metódus pontosan ezen az útvonalon halad át

Egy viselkedési pontot tisztázni kell: a vágás kivág, nem pedig méretez. Az oldal továbbra is a kért DPI mellett rasztereződi a normál pozíciójában, és a vágási régió egyszerűen elvet mindent a téglalapon kívül. Nem nagyítjuk fel a régiót, hogy kitöltse a kimenetet; hanem egy ablakot vágunk ki a teljes felbontású leképezésből. Ha nagyobb méretben szeretné a régiót, növelje a DPI-t. A téglalap koordinátái eszköz-térben (device space) kerülnek értelmezésre a pont-képpont skálázás után, a leképezett felület bal felső sarkától mérve, így a Left és Top értékeket az oldal tetejétől lefelé tervezze. Ha részletesebben szeretné megismerni, hogyan vezérli a PDFlibPas az eszköz-kontextust a képernyős kimenethez, a nyomtatási előnézetről és az eszköz-kontextus kimenetéről szóló kísérőcikk a megjelenítési oldalról mutatja be ugyanezt a DC felépítést

A becsületes határ: 1 bites BMP, nem pedig G4 TIFF

Könnyű lenne ezt „fax-kész kimenetként” eladni, ezért álljon itt a korlát egyértelműen kimondva. A RenderPageToMonochromeFile egy pf1bit BMP-t állít elő. Nem készít CCITT Group 4 TIFF-et, ami egy valós fax munkafolyamat vagy egy TIFF archívum által elvárt formátum lenne. Az ok konkrét, nem pedig figyelmetlenség: a PDFlibPas CCITT egysége jelenleg dekódolja a G4 folyamokat, de nem rendelkezik G4 kódolóval (encoder). Kódoló nélkül nincs hova írni a tömörített monokróm szakaszokat, így a monokróm útvonal megáll a tömörítetlen 1 bites DIB-nél

A gyakorlatban ez még mindig hasznos. Az 1 bites BMP a megfelelő képpontformátum, szórva és előkészítve, és a legtöbb fax-, archiváló vagy OCR-eszközlánc szívesen fogadja azt, vagy maguk konvertálják át G4-re egy további lépésben. De ha a követelmény szó szerint egy közvetlenül a könyvtárból származó Group 4 TIFF, akkor ez még nem az, és meg kell terveznie a saját tömörítési szakaszát. Tudni, hogy hol áll meg egy funkció, ugyanolyan fontos, mint tudni, hogy mit csinál

Mindkét metódus szándékosan kicsi, és ez a tervezési tanulság, amit érdemes levonni erről az oldalról: egy a leképező tetején ülő kényelmi API valódi képességeket — monokróm kimenetet, régióvágást — adhat hozzá anélkül, hogy bele kellene nyúlni a raszterezőbe, és destabilizálni kellene minden más hívót. Amikor az alatta lévő raszterezéshez leképező motorok közül kell választani, a többmotoros PDF-leképezés Delphiben című áttekintés részletesen bemutatja a kompromisszumokat. A teljes leképezési felület és az API többi részének megtekintéséhez a PDFlibPas Delphi PDF Library termékoldala nyújt teljes képet