O poartă fax nu vrea randarea ta de pagină pe 24-bit. Nici pipeline-ul de arhivare care stochează un milion de facturi care par scanate, nici front-endul OCR care aplică prag la totul în alb și negru înainte să se uite măcar după un caracter. Toate trei vor același lucru: un bitmap curat de 1 bit, un bit pe pixel, în care fiecare punct este fie cerneală, fie hârtie. Dacă le dai un BMP full color, vor arunca oricum 23 de biți pe pixel, de obicei cu un pas de dithering mai slab decât ai fi putut face tu. Întrebarea interesantă este unde ar trebui să aibă loc acea conversie descendentă, iar răspunsul în PDFlibPas ajunge să spună ceva util despre cum să extinzi un renderer pe care ai prefera să nu-l rescrii
PDFlibPas este o bibliotecă PDF nativă în Object Pascal pentru Delphi și C++Builder. Nucleul său de randare rasterizează o pagină într-un bitmap și poate emite BMP, PNG, JPEG, WMF și încă câteva formate. Ce nu putea face până de curând era să returneze un bitmap cu adevărat monocrom sau să redea doar o parte dintr-o pagină. Ambele au sosit în v3.83.0, și ambele au fost construite ca straturi subțiri de conveniență peste rendererul existent, nu ca schimbări ale rasterizatorului în sine. Această constrângere este întreaga poveste
De ce conversia descendentă după randare, nu în interiorul rendererului
Cea mai evidentă cale de a produce o imagine de 1 bit este să-i spui rasterizatorului să deseneze în 1 bit. Aceasta este și calea care strică restul. Bitmapul intern al rendererului este creat cu un PixelFormat := pf24bit în PDFlibRenderer constructor, iar acea suprafață de 24-bit este partajată de toate căile de randare: export PNG, previzualizarea device-context, ieșirea JPEG, totul. Dacă îl schimbi în pf1bit la sursă, nu ai adăugat o funcție monocromă, ci ai degradat fidelitatea culorilor pentru fiecare apelant din bibliotecă și te-ai angajat să depanezi o duzină de regresii în aval
Așa că RenderPageToMonochromeFile ia calea opusă. Randează pagina normal, într-un BMP temporar pe 24-bit, și abia apoi o reduce la 1 bit ca pas de postprocesare. Rendererul rămâne neatins. Comportamentul monocrom trăiește în întregime în metoda de conveniență, ceea ce înseamnă că nu poate afecta pe nimeni care nu o apelează. Acesta este genul de compromis care merită numit explicit: un postproces plătește o alocare suplimentară de bitmap și un fișier temporar, iar în schimb păstrează un nucleu esențial complet în afara domeniului de impact. Pentru o funcție care există ca să servească cazurile-limită de fax și arhivare, aceasta este partea corectă a balanței
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;
Cum are loc de fapt reducerea la 1 bit
Conversia descendentă se sprijină pe GDI, nu pe o buclă de prag scrisă de mână, iar alegerea contează pentru calitatea ieșirii. În interiorul metodei, bitmapul temporar pe 24-bit este încărcat într-un TBitmap, un al doilea TBitmap este creat cu PixelFormat := pf1bit la aceleași dimensiuni, iar pixelii se mută cu un singur blit:
// 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');
Trucul este SetStretchBltMode cu HALFTONE. Deși sursa și destinația au aceeași dimensiune, deci nu are loc nicio scalare, modul stretch controlează în continuare cum mapează GDI culorile în paleta de 1 bit. HALFTONE face să aplice dithering halftone, transformând zonele gri și marginile de text antialiasat în modele de puncte negre și albe, în loc de o tăiere dură la cea mai apropiată dintre cele două culori. Dacă scoți apelul de setare a modului sau folosești valoarea implicită BLACKONWHITE, conținutul în tonuri de gri se posterizează în forme blocate, praguite. Pentru ieșirea destinată documentelor scanate și preprocesării OCR, rezultatul cu dithering este aproape întotdeauna ceea ce vrei
Un detaliu este nenegociabil și ușor de greșit: randarea temporară trebuie să fie un BMP. RenderPageToMonochromeFile apelează rendererul general cu un cod de opțiune 0, care este BMP. Argumentul options de pe RenderPageToFile este un enum mic de întregi, iar valorile nu sunt interschimbabile pentru acest scop: 0 este BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG și așa mai departe. Conversia descendentă apoi face TBitmap.LoadFromStream asupra fișierului temporar. Dacă îi dai un WMF, prin trecerea 2, iar acea încărcare aruncă "Bitmap image is not valid", deoarece un Windows Metafile este un flux de înregistrări vectoriale, nu un DIB. Conversia descendentă monocromă este o operație raster de la un capăt la altul, deci intermediarul trebuie să fie un format raster
Randarea doar a unei subregiuni dintr-o pagină
A doua metodă, RenderPageRegionToFile, randează doar un dreptunghi al paginii în locul întregii pagini. Cazurile de utilizare sunt familiare odată ce ai construit orice vizualizator de documente: decuparea unui bloc de semnătură dintr-un contract, generarea unui tile pentru o hartă mărită a unui desen mare sau extragerea unei regiuni ștampilate pentru o miniatură, fără a plăti costul rasterizării întregii pagini la DPI mare. Semnătura este simplă:
// 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');
Șirul de decupare este format din patru valori double separate prin virgulă, în puncte PDF, analizat manual în interiorul metodei pentru a ocoli particularitățile de localizare și DelimitedText ciudățeniile. Din lățime și înălțime, metoda calculează dimensiunea bitmapului de ieșire ca Round(Width * DPI / 72) pe Round(Height * DPI / 72), alocă un pf24bit bitmap în memorie de exact acea dimensiune și îl redă în contextul său de dispozitiv prin RenderPageToDCClip. Fișierul rezultat conține doar dreptunghiul decupat, dimensionat după regiune, nu după întreaga pagină
Parametrul clip care nu făcea nimic
Aici munca a fost mai precisă decât pare. RenderPageToDCClip a purtat mult timp un Clip parametru și a fost o minciună. Apelul accepta argumentul, îl transmitea mai departe către TPDFPageTree.RenderPageToDC, iar acea implementare îl ignora complet, fără să-l ofere vreodată rendererului. Puteai transmite orice dreptunghi doreai și primeai întreaga pagină înapoi. Oricine conectase RenderPageToDCClip așteptând o decupare primea o randare pe toată pagina și, în funcție de machetă, poate că nici nu observa
v3.83.0 a făcut legătura. RenderPageToDC parsează acum același "Left,Top,Width,Height" dreptunghi în puncte și îl aplică drept o regiune reală de clip GDI pe contextul de dispozitiv țintă înainte ca rendererul să deseneze. Conversia din puncte în pixeli de dispozitiv folosește obișnuitul factor de scalare DPI / 72 aplicat tuturor celor patru margini. Secvența din jurul randării este rutina standard salvare/clip/restaurare:
// 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);
Perechea SaveDC / RestoreDC(-1) este ceea ce face această funcție sigură pentru apeluri repetate: regiunea de clip este împinsă în stiva de stare a DC-ului, pagina este desenată, iar clip-ul original este readus, indiferent de modul în care se termină randarea. RestoreDC(TargetDC, -1) restaurează cea mai recent salvată stare, care este idiomul standard pentru un save/restore echilibrat. Sare peste restaurare și un apelant care refolosește același DC pentru o randare ulterioară pe toată pagina ar descoperi că este, în mod misterios, decupat la ultima regiune. Repararea parametrului mort a rezolvat și RenderPageRegionToFile gratuit, deoarece noua metodă trece exact prin această cale
Un aspect comportamental de reținut: clip-ul decupează, nu scalează. Pagina este încă rasterizată la DPI-ul cerut, în poziția ei normală, iar regiunea de clip elimină pur și simplu tot ce se află în afara dreptunghiului. Nu mărești regiunea ca să umpli ieșirea; tai o fereastră din randarea la rezoluție maximă. Dacă vrei o regiune mărită, crește DPI-ul. Coordonatele dreptunghiului sunt interpretate în spațiul dispozitivului după scalarea din puncte în pixeli, măsurate din colțul stânga-sus al suprafeței randate, așa că planifică-ți Left și Top de la partea de sus a paginii în jos. Pentru o incursiune mai profundă în modul în care PDFlibPas conduce un context de dispozitiv pentru ieșirea pe ecran, articolul companion despre previzualizarea la imprimare și ieșirea prin device context parcurge aceeași instalație DC din perspectiva afișării
Limita onestă: BMP pe 1 bit, nu TIFF G4
Ar fi ușor să fie prezentat excesiv de ambițios drept „ieșire gata de fax”, așa că iată limita spusă pe față. RenderPageToMonochromeFile produce un pf1bit BMP pe 1 bit. Nu produce un TIFF CCITT Group 4, care este formatul pe care îl așteaptă în mod normal un flux de lucru fax sau o arhivă TIFF. Motivul este concret, nu o omisiune: unitatea CCITT din PDFlibPas decodifică în prezent fluxuri G4, dar nu are un encoder. Fără un encoder nu există unde să scrii secvențe monocrome comprimate, așa că traseul monocrom se oprește la un DIB pe 1 bit necomprimat
În practică, asta este totuși util. Un BMP pe 1 bit este formatul corect de pixeli, deja ditherizat și gata, iar majoritatea fluxurilor fax, de arhivare sau OCR îl pot ingera cu plăcere sau îl pot converti ele însele în G4 cu un singur pas ulterior. Dar dacă cerința ta este literalmente un TIFF Group 4 direct din bibliotecă, acesta încă nu este acela, și ar trebui să-ți planifici propriul pas de compresie. Să știi unde se oprește o funcționalitate valorează la fel de mult ca să știi ce face
Ambele metode sunt deliberat mici, și aceasta este lecția de design care merită luată de pe această pagină: o API de conveniență care stă deasupra unui renderer poate adăuga capabilități reale, ieșire monocromă, decupare pe regiune, fără să intre în rasterizer și să destabilizeze fiecare alt apelant. Când chiar trebuie să alegi între motoare de randare pentru rasterizarea de bază, prezentarea generală a randării PDF multi-motor în Delphi acoperă în profunzime compromisurile. Pentru a vedea suprafața completă de randare și restul API-ului, PDFlibPas Delphi PDF Library pagina produsului oferă imaginea completă