Odborný článok

Renderovanie PDF strán do 1-bit monochrome v Delphi

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 PDF Library for Delphi hovorí niečo užitočné o tom, ako rozšíriť renderer, ktorý radšej nechcete prepisovať

PDF Library for Delphi 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

Pipeline PDF Library for Delphi ukazujúca stranu PDF vykreslenú do dočasného 24-bitového BMP, zvrásnenú GDI HALFTONE blit do 1-bitovej monochromatickej bitmapy a skonzumovanú pracovnými tokmi faxu, archivácie a OCR
Nové metódy najprv vykreslia plnofarebnú stranu a potom skonvertujú hotový raster nadol mimo renderer. Faxové brány, archívne úložiská a front-endy OCR dostávajú skutočnú pf1bit bitmapu
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI je klasické faxové rozlíšenie Group 4; index strany je 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:

PDF Library for Delphi: Detail GDI kolapsu porovnávajúci HALFTONE stretch blit, ktorý rozrazí odtiene sivej do bodkových vzorov, s blokovým predvoleným prahom BLACKONWHITE
Vo vnútri RenderPageToMonochromeFile jeden StretchBlt presúva každý pixel na pf1bit plochu identickej veľkosti. Pri aktívnom HALFTONE sa šedé menia na roztrasené vzory bodiek namiesto hranatých tvarov, ktoré produkuje predvolený prah
// vnútri RenderPageToMonochromeFile, po načítaní 24-bitovej ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE povie GDI, aby ditherovalo 24-bitový zdroj nadol na 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:

PDF Library for Delphi: Vykreslenie výrezu v bodoch PDF: obdĺžnik 72,72,180,72 na vykreslenej strane PDF v plnom rozlíšení sa stane bitmapou 375 krát 150 pixelov pri 150 DPI, pretože výrez orezáva namiesto škálovania
RenderPageRegionToFile vyreže okno merané v PDF bodoch z renderu v plnom rozlíšení. Výstupná bitmapa má veľkosť z width a height krát DPI delené 72, nikdy zo zmenšenej celej strany
// Clip je "Left,Top,Width,Height" v bodoch PDF (72 pt = 1 palec)
// Tu: box 2,5 x 1 palca, jeden palec od ľavého horného rohu strany
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:

// vnútri TPDFPageTree.RenderPageToDC, keď Clip nie je prázdny
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer tu vykreslí stranu ...
// v bloku finally:
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 PDF Library for Delphi 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 PDF Library for Delphi 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 PDF Library for Delphi Delphi PDF Library kde je celý obraz doplnený do detailu