Fax gateway nechce váš 24-bit render strany. Nechce ho ani archivačný pipeline, ktorý ukladá milión faktúr vyzerajúcich ako sken, ani OCR front-end, ktorý všetko ešte pred rozpoznávaním prahu na čiernobiele. Všetky tri chcú to isté: čistý 1-bit bitmap, jeden bit na pixel, kde je každý bod buď atrament, alebo papier. Dajte im plnofarebný BMP a ony aj tak zahodia 23 bitov na pixel, často s horším dithering prechodom, než aký by ste vedeli spraviť sami. Zaujímavá otázka teda znie, kde sa má down-conversion stať, a odpoveď v PDFlibPas hovorí niečo užitočné o tom, ako rozšíriť renderer, ktorý radšej nechcete prepisovať
PDFlibPas je natívna Object Pascal PDF knižnica pre Delphi a C++Builder. Jej rendering core rasterizuje stranu do bitmapy a vie emitovať BMP, PNG, JPEG, WMF a pár ďalších formátov. Čo však donedávna nevedela, bolo vrátiť skutočný monochrome bitmap alebo renderovať len časť strany. Obe veci prišli vo verzii v3.83.0 a obe boli postavené ako tenké convenience vrstvy nad existujúcim rendererom, nie ako zmeny v samotnom rasterizéri. Toto obmedzenie je celý príbeh
Prečo down-convertovať po renderovaní, nie vo vnútri rendereru
Zjavný spôsob, ako vyrobiť 1-bit obrázok, je povedať rasterizéru, aby kreslil v 1-bit režime. Je to zároveň spôsob, ktorý rozbije všetko ostatné. Interný bitmap rendereru sa vytvára s natvrdo daným PixelFormat := pf24bit v konštruktore PDFlibRenderer, a tento 24-bit surface zdieľajú všetky render cesty: export PNG, device-context preview, výstup JPEG a všetko ostatné. Prepnite ho pri zdroji na pf1bit a nepridali ste monochrome feature, ale zhoršili ste farebnú vernosť pre každého volajúceho v celej knižnici a podpísali ste sa pod debugovanie tucta downstream regresií
Preto RenderPageToMonochromeFile volí opačnú cestu. Stranu vyrenderuje normálne do dočasného 24-bit BMP a až potom ju zbalí na 1-bit ako post-processing krok. Renderer zostáva nedotknutý. Monochrome správanie žije celé v convenience metóde, čo znamená, že nemôže ovplyvniť nikoho, kto ju nevolá. Presne takýto trade-off stojí za explicitné pomenovanie: post-process zaplatí jednou extra alokáciou bitmapy a dočasným súborom, no výmenou ponechá nosné jadro úplne mimo scope. Pre feature, ktorá slúži faxovým a archivačným edge caseom, je to správna strana účtovnej knihy
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;
Ako sa 1-bit zbalenie v skutočnosti robí
Down-conversion sa opiera o GDI, nie o ručne písaný threshold loop, a táto voľba je dôležitá pre kvalitu výstupu. Vo vnútri metódy sa dočasná 24-bit bitmap načíta do TBitmap, druhý TBitmap sa vytvorí s PixelFormat := pf1bit v rovnakých rozmeroch a pixely sa presunú jediným blitom:
// 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');
Trik je v SetStretchBltMode s režimom HALFTONE. Aj keď sú zdroj aj cieľ rovnako veľké a nedochádza teda k škálovaniu, stretch mode stále riadi, ako GDI mapuje farby do 1-bit palety. HALFTONE ho prinúti použiť halftone dithering, ktorý z šedých plôch a antialiasovaných hrán textu spraví vzory čiernych a bielych bodov namiesto tvrdého orezania na najbližšiu z dvoch farieb. Ak volanie režimu vynecháte alebo použijete predvolený BLACKONWHITE, grayscale obsah sa rozpadne na blokové prahované tvary. Pre výstup zameraný na skenované dokumenty a OCR preprocessing je ditherovaný výsledok takmer vždy to, čo chcete
Jeden detail je nevyjednateľný a dá sa ľahko pokaziť: dočasný render musí byť BMP. RenderPageToMonochromeFile volá všeobecný renderer s options code 0, čo znamená BMP. Argument options pri RenderPageToFile je malý integer enum a jeho hodnoty sa na tento účel nedajú zamieňať: 0 je BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG a tak ďalej. Down-converter potom robí TBitmap.LoadFromStream nad dočasným súborom. Ak mu podsuniete WMF tak, že odovzdáte 2, načítanie skončí chybou "Bitmap image is not valid", pretože Windows Metafile je vektorový záznamový stream, nie DIB. Monochrome down-conversion je od začiatku do konca raster operácia, takže medziformát musí byť raster
Renderovanie len podoblasti strany
Druhá metóda, RenderPageRegionToFile, renderuje len obdĺžnik strany namiesto celej strany. Use casey sú známe každému, kto staval document viewer: orezanie podpisového bloku zo zmluvy, vygenerovanie dlaždice pre priblíženú mapu veľkého výkresu alebo vytiahnutie jedného opečiatkovaného regiónu pre thumbnail bez toho, aby ste platili render celej strany pri vysokom DPI. Signatúra je priamočiara:
// 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');
Clip string sú štyri comma-separated double hodnoty v PDF bodoch, ktoré sa vnútri metódy parsujú ručne, aby sa obišli locale a DelimitedText zvláštnosti. Zo šírky a výšky metóda vypočíta veľkosť výstupnej bitmapy ako Round(Width * DPI / 72) krát Round(Height * DPI / 72), alokuje in-memory bitmap typu pf24bit presne týchto rozmerov a renderuje do jej device context cez RenderPageToDCClip. Výsledný súbor obsahuje iba orezaný obdĺžnik, pričom jeho rozmery zodpovedajú regiónu, nie celej strane
Clip parameter, ktorý nič nerobil
Tu bola práca ostrejšia, než na prvý pohľad vyzerá. RenderPageToDCClip nieslo parameter Clip už dlho a bola to lož. Volanie argument prijalo, poslalo ho ďalej do TPDFPageTree.RenderPageToDC, a implementácia ho úplne ignorovala, nikdy ho neodovzdala rendereru. Mohli ste poslať akýkoľvek rectangle a dostali ste celú stranu. Ktokoľvek, kto zapojil RenderPageToDCClip v očakávaní cropu, dostával render celej strany a podľa layoutu si to možno ani nevšimol
Verzia v3.83.0 tento vodič konečne zapojila. RenderPageToDC teraz parsuje ten istý obdĺžnik v bodoch zo "Left,Top,Width,Height" a aplikuje ho ako skutočný GDI clip region na target device context ešte predtým, než renderer kreslí. Konverzia z bodov na device pixely používa obvyklý scale factor DPI / 72 aplikovaný na všetky štyri hrany. Postup okolo renderu je štandardný save/clip/restore tanec:
// 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);
Dvojica SaveDC / RestoreDC(-1) robí toto volanie bezpečným pri opakovanom použití: clip region sa uloží na zásobník stavu DC, strana sa vykreslí a pôvodný clip sa bez ohľadu na výsledok renderu vráti späť. RestoreDC(TargetDC, -1) obnoví naposledy uložený stav, čo je štandardný idiom vyváženého save/restore. Keby sa restore vynechalo, volajúci, ktorý ten istý DC znovu použije pre následný full-page render, by zrazu zistil, že je záhadne orezaný na posledný región. Oprava nefunkčného parametra mimochodom automaticky opravila aj RenderPageRegionToFile, pretože nová metóda ide presne cez tú istú cestu
Jeden behaviorálny bod treba dostať pod kožu: clip orezáva, nie škáluje. Strana sa stále rasterizuje pri vami zadanom DPI v normálnej pozícii a clip region jednoducho odhodí všetko mimo obdĺžnika. Región teda nepribližujete tak, aby vyplnil výstup, ale vyrezávate okno z full-resolution renderu. Ak chcete región zväčšiť, zvýšte DPI. Súradnice obdĺžnika sa interpretujú v device space po prevode points-to-pixels, merané od ľavého horného rohu vykresleného surface, takže svoje Left a Top plánujte od vrchu strany smerom nadol. Pre hlbšiu prehliadku toho, ako PDFlibPas ovláda device context pri on-screen výstupe, vás sprievodný článok o print preview a device-context output prevedie rovnakým DC plumbing z pohľadu zobrazenia
Poctivá hranica: 1-bit BMP, nie G4 TIFF
Bolo by ľahké predávať to ako "výstup pripravený pre fax", preto tu hranicu povedzme úplne priamo. RenderPageToMonochromeFile vytvára pf1bit BMP. Nevytvára CCITT Group 4 TIFF, čo je formát, ktorý skutočný faxový workflow alebo TIFF archív zvyčajne očakáva. Dôvod je konkrétny, nie zanedbaný: CCITT unit v PDFlibPas dnes vie G4 streamy dekódovať, ale nemá G4 encoder. Bez encoderu nie je kam zapisovať komprimované monochrome behy, takže monochrome cesta končí pri nekomprimovanom 1-bit DIB
V praxi je to stále užitočné. 1-bit BMP je správny pixel format, je ditherovaný a pripravený, a väčšina faxových, archivačných alebo OCR toolchainov ho ochotne prijme alebo si ho v ďalšom kroku sama skonvertuje na G4. Ak však vaša požiadavka doslova znie Group 4 TIFF priamo z knižnice, toto zatiaľ nie je ono a musíte si naplánovať vlastný compression stage. Vedieť, kde feature končí, má rovnakú hodnotu ako vedieť, čo vie
Obe metódy sú zámerne malé a to je dizajnová lekcia, ktorú si z tejto strany stojí za to odniesť: convenience API sediace nad rendererom vie pridať skutočné schopnosti, monochrome output a region cropping, bez toho, aby zasahovalo do rasterizéra a destabilizovalo každého ďalšieho volajúceho. Keď si naopak potrebujete vybrať medzi rendering engines pre samotnú rasterizáciu, prehľad multi-engine PDF rendering v Delphi rozoberá trade-offy do hĺbky. Plný rendering surface a zvyšok API nájdete na stránke PDFlibPas Delphi PDF Library kde je celý obraz doplnený do detailu