Un gateway fax non vuole il tuo render di pagina a 24 bit. Nemmeno la pipeline di archiviazione che conserva un milione di fatture dall'aspetto scansionato, né il front-end OCR che soglia tutto in bianco e nero prima ancora di cercare un carattere. Tutti e tre vogliono la stessa cosa: una bitmap pulita a 1 bit, un bit per pixel, in cui ogni punto è inchiostro o carta. Se gli passi un BMP a colori, getteranno comunque via 23 bit per pixel, di solito con un passaggio di dithering peggiore di quello che avresti potuto fare tu. La domanda interessante è dove debba avvenire quella conversione verso il basso, e la risposta in PDFlibPas finisce per dire qualcosa di utile su come estendere un renderer che preferiresti non riscrivere
PDFlibPas è una libreria PDF nativa in Object Pascal per Delphi e C++Builder. Il suo core di rendering rasterizza una pagina in una bitmap e può produrre BMP, PNG, JPEG, WMF e una manciata di altri formati. Fino a poco tempo fa non restituiva una vera bitmap monocromatica, né renderizzava solo una parte di una pagina. Entrambe le cose sono arrivate in v3.83.0, e entrambe sono state costruite come sottili layer di convenienza sopra il renderer esistente anziché come modifiche al rasterizzatore stesso. Questo vincolo è tutta la storia
Perché fare la conversione verso il basso dopo il rendering, non dentro il renderer
Il modo ovvio per produrre un'immagine a 1 bit è dire al rasterizzatore di disegnare in 1 bit. È anche il modo per rompere tutto il resto. La bitmap interna del renderer viene creata con un `PixelFormat := pf24bit` hardcoded nel costruttore `PDFlibRenderer`, e quella superficie a 24 bit è condivisa da ogni percorso di rendering: esportazione PNG, anteprima del device context, output JPEG, tutto. Se la porti a `pf1bit` alla fonte, non hai aggiunto una funzione monocromatica, hai peggiorato la fedeltà cromatica per tutti i chiamanti della libreria e ti sei impegnato a debuggare una dozzina di regressioni a valle PixelFormat := pf24bit in the PDFlibRenderer constructor, and that 24-bit surface is shared by every render path: PNG export, the device-context preview, JPEG output, all of it. Flip it to pf1bit at the source and you have not added a monochrome feature, you have degraded color fidelity for every caller in the library and signed up to debug a dozen downstream regressions
Quindi RenderPageToMonochromeFile prende la strada opposta. Renderizza normalmente la pagina, in un BMP temporaneo a 24 bit, e solo dopo la comprime a 1 bit come fase di post-elaborazione. Il renderer non viene toccato. Il comportamento monocromatico vive interamente nel metodo di convenienza, il che significa che non può influenzare nessuno che non lo chiami. È il tipo di compromesso che vale la pena nominare esplicitamente: un post-process paga una allocazione bitmap in più e un file temporaneo, e in cambio tiene un core portante completamente fuori dal perimetro. Per una funzione che esiste per servire casi limite di fax e archiviazione, è il lato giusto del bilancio
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;
Come avviene davvero la compressione a 1 bit
La conversione verso il basso si appoggia a GDI invece che a un ciclo threshold scritto a mano, e la scelta conta per la qualità dell'output. Dentro il metodo la bitmap temporanea a 24 bit viene caricata in un TBitmap, una seconda TBitmap viene creata con PixelFormat := pf1bit alle stesse dimensioni, e i pixel passano con una sola 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');
Il trucco è SetStretchBltMode con HALFTONE. Anche se sorgente e destinazione hanno le stesse dimensioni, quindi non avviene alcuna scalatura, la modalità di stretch governa comunque il modo in cui GDI mappa i colori nella palette a 1 bit. HALFTONE fa applicare il dithering halftone, trasformando le regioni grigie e i bordi del testo antialias in pattern di punti bianchi e neri invece che in un taglio netto al colore più vicino tra due BLACKONWHITE, e il contenuto in scala di grigi si posterizza in forme a blocchi sogliate. Per output di documenti scansionati e di pre-elaborazione OCR, il risultato con dithering è quasi sempre quello che vuoi
Un dettaglio non è negoziabile ed è facile sbagliarsi: il rendering temporaneo deve essere un BMP. RenderPageToMonochromeFile chiama il renderer generale con un codice opzioni pari a 0, cioè BMP. L'argomento delle opzioni di RenderPageToFile è un piccolo enum intero, e i valori non sono intercambiabili per questo scopo: 0 è BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, e così via. Il convertitore verso il basso esegue quindi TBitmap.LoadFromStream sul file temporaneo. Dagli in pasto un WMF, passando 2, "Bitmap image is not valid", perché un Windows Metafile è uno stream di record vettoriali, non un DIB. La conversione monocromatica da un estremo all'altro è un'operazione raster, quindi l'intermedio deve essere un formato raster
Renderizzare solo una sotto-regione di una pagina
Il secondo metodo, RenderPageRegionToFile, renderizza solo un rettangolo della pagina invece dell'intero contenuto. I casi d'uso sono familiari non appena hai costruito un qualsiasi visualizzatore di documenti: ritagliare un blocco firma da un contratto, generare una tile per la mappa ingrandita di un disegno grande, oppure estrarre una regione timbrata per una miniatura senza pagare il costo di rasterizzare l'intera pagina ad alto DPI. La firma è semplice:
// 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');
La stringa del clip è composta da quattro double separati da virgole in punti PDF, analizzati manualmente dentro il metodo per aggirare le stranezze della locale e di DelimitedText . Dalla larghezza e dall'altezza il metodo calcola la dimensione della bitmap di output come Round(Width * DPI / 72) per Round(Height * DPI / 72), alloca una bitmap pf24bit in memoria esattamente di quella dimensione e renderizza nel suo device context attraverso RenderPageToDCClip. Il file risultante contiene solo il rettangolo ritagliato, dimensionato sulla regione e non sull'intera pagina
Il parametro clip che non faceva nulla
Qui il lavoro è stato più netto di quanto sembri. RenderPageToDCClip portava da tempo un parametro Clip , ed era una bugia. La chiamata accettava l'argomento, lo passava a TPDFPageTree.RenderPageToDC, e quell'implementazione lo ignorava completamente, senza mai consegnarlo al renderer. Potevi passare qualsiasi rettangolo volessi e riottenere l'intera pagina. Chiunque avesse collegato RenderPageToDCClip aspettandosi un ritaglio stava ricevendo un render dell'intera pagina e, a seconda del layout, forse non se ne era nemmeno accorto
v3.83.0 ha collegato il filo. RenderPageToDC ora analizza lo stesso "Left,Top,Width,Height" rettangolo di punti e lo applica come vero clip region GDI sul device context di destinazione prima che il renderer disegni. La conversione da punti a pixel del dispositivo usa il consueto DPI / 72 fattore di scala, applicato a tutti e quattro i bordi. La sequenza attorno al render è la solita danza save/clip/restore:
// 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);
Il SaveDC / RestoreDC(-1) è la coppia che rende sicura la chiamata ripetuta: il clip region viene spinto sullo stack di stato del DC, la pagina viene disegnata e il clip originale viene ripristinato a prescindere da come termina il render. RestoreDC(TargetDC, -1) ripristina lo stato salvato più di recente, che è l'idioma standard per un save/restore bilanciato. Se salti il restore, un chiamante che riutilizza lo stesso DC per un render completo successivo si ritroverebbe misteriosamente ritagliato sull'ultima regione. Correggere il parametro morto ha sistemato RenderPageRegionToFile gratis, visto che il nuovo metodo passa esattamente da questo percorso
Un punto comportamentale da interiorizzare: il clip ritaglia, non scala. La pagina viene ancora rasterizzata al DPI che hai richiesto, nella sua posizione normale, e il clip region semplicemente scarta tutto ciò che sta fuori dal rettangolo. Non stai zoomando la regione per farla riempire l'output; stai ritagliando una finestra dal render a piena risoluzione. Se vuoi una regione ingrandita, alza il DPI. Le coordinate del rettangolo sono interpretate nello spazio del dispositivo dopo la scalatura da punti a pixel, misurate dall'angolo in alto a sinistra della superficie renderizzata, quindi pianifica il tuo Left e Top dall'alto della pagina verso il basso. Per un tour più approfondito di come PDFlibPas guida un device context per l'output a schermo, il pezzo complementare su anteprima di stampa e output device-context percorre lo stesso plumbing DC dal lato della visualizzazione
Il confine onesto: BMP a 1 bit, non TIFF G4
Sarebbe facile venderlo come "output pronto per il fax", quindi ecco il limite detto in modo chiaro. RenderPageToMonochromeFile produce un pf1bit BMP. Non produce un TIFF CCITT Group 4, che è il formato che un vero flusso fax o un archivio TIFF di solito si aspetta. Il motivo è concreto e non un'omissione: l'unità CCITT di PDFlibPas attualmente decodifica stream G4 ma non ha un G4 encoder. Senza un encoder non c'è dove scrivere run monocromatici compressi, quindi il percorso monocromatico si ferma a un DIB a 1 bit non compresso
In pratica resta comunque utile. Un BMP a 1 bit ha il formato pixel corretto, con dithering e pronto all'uso, e la maggior parte delle pipeline fax, archivio o OCR lo ingerirà volentieri o lo convertirà in G4 con un solo passaggio a valle. Ma se il tuo requisito è letteralmente un TIFF Group 4 diretto dalla libreria, questo non lo è ancora, e dovresti prevedere un tuo stadio di compressione. Sapere dove una funzione si ferma vale quanto sapere cosa fa
Entrambi i metodi sono deliberatamente piccoli, e questa è la lezione di design da portarsi via da questa pagina: una API di convenienza che si appoggia a un renderer può aggiungere una capacità reale, output monocromatico, ritaglio di regioni, senza infilarsi nel rasterizzatore e destabilizzare tutti gli altri chiamanti. Quando devi davvero scegliere tra motori di rendering per la rasterizzazione sottostante, la panoramica su rendering PDF con più motori in Delphi approfondisce i compromessi. Per vedere l'intera superficie di rendering e il resto dell'API, la PDFlibPas Delphi PDF Library pagina prodotto ha il quadro completo