HotPDF espone tre kernel di downsampling immagini tramite la proprietà ImageDownsampleKernel e una passata Floyd-Steinberg separata tramite RenderOutputDither. La prima controlla come appaiono le fotografie dopo che le hai rimpicciolite per rispettare un budget di dimensione, la seconda controlla come appaiono dopo che una pagina è stata ridotta a bianco e nero. Nessuna è attiva per default, ed entrambe sono opt-in per lo stesso motivo: costano tempo vero
La pressione che porta fin qui è nota. Un contratto scansionato da 60 MB deve uscire attraverso un gateway di posta che rifiuta tutto ciò che supera i 10 MB, oppure un lotto di estratti conto deve finire su un dispositivo monocromatico in stile fax che renderizza ogni pixel grigio come carta o toner. Entrambi i problemi sono problemi di resampling, e entrambi hanno una risposta veloce che sembra brutta e una risposta lenta che sembra giusta
In cosa differiscono davvero i tre kernel
THPDFResampleKernel ha tre valori, e stanno in punti genuinamente diversi della curva velocità-qualità. rkHalftone delega allo storico percorso GDI StretchBlt con la modalità HALFTONE, che nonostante il nome è un filtraggio di classe bilineare: veloce, adeguato per line art e screenshot, e incline ai bordi granulosi che riconosci all'istante sulle fotografie rimpicciolite. rkBicubic esegue un kernel Catmull-Rom separabile, e rkLanczos3 esegue un sinc finestrato separabile con supporto a tre lobi
Entrambi i kernel separabili girano in due passate, orizzontale poi verticale, con 6-12 tap per pixel di destinazione in Pascal puro. È all'incirca un ordine di grandezza più lento del percorso GDI, ed è esattamente per questo che rkHalftone resta il default. Su un batch notturno di migliaia di pagine la differenza è una decisione di schedulazione, non una preferenza. Su un singolo documento per cui un utente sta aspettando, Lanczos3 costa quasi niente e si vede che è meglio
Due proprietà implementative valgono la pena conoscere perché determinano ciò che l'output può e non può fare. I bordi serrano per replicazione del bordo invece che con wrap o dissolvenza, e i pesi sono normalizzati per pixel di destinazione. Insieme, le due cose significano che il risultato non suona mai sotto il nero né sopra il bianco, così il classico alone di overshoot di Lanczos attorno a un bordo netto non compare come artefatti tagliati nell'immagine codificata
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // impostare prima della chiamata
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
L'argomento MinimumSavingsBytes, 4096 sopra, è la guardia che tiene l'operazione onesta. Ricodificare un'immagine già compressa in modo efficiente può produrre uno stream più grande dell'originale, e un downsampler che sostituisce ciecamente ogni immagine ogni tanto ingrandisce il file che gli era stato chiesto di ridurre. La soglia dice: commetti la sostituzione solo quando risparmia almeno questi byte. PreservedCalibratedImageCount riporta l'altra decisione conservativa, le immagini lasciate intatte perché portano uno spazio colore calibrato che il resampling comprometterebbe
Perché un coefficiente polinomiale sbagliato è così difficile da vedere?
Perché un kernel di interpolazione rotto non va in crash né lancia eccezioni, produce solo un'immagine che sembra sottilmente sbagliata in un modo che nessuno sa attribuire. Il kernel Catmull-Rom è cubico a tratti, e il suo ramo esterno in forma di Horner annidata è ((-0.5t + 2.5)t - 4)t + 2. Scrivi quel coefficiente centrale come -5 invece di -4 e la funzione valuta ancora, restituisce ancora numeri in un intervallo plausibile, e produce ancora un'immagine
Il danno si manifesta come W(1) che valuta -1 dove dev'essere 0. I pesi negativi si accumulano, la somma satura a zero, e il sintomo visibile è un gradiente il cui estremo sinistro va al nero e uno step edge che perde i suoi toni intermedi. Nulla nel guasto punta a un polinomio. Il controllo che lo intercetta in pochi secondi è aritmetico e non visivo: un kernel interpolante deve soddisfare W(0) = 1 e W(±1) = W(±2) = 0, e qualunque kernel che manchi quei tre punti ha un errore di coefficiente, punto. Aserra quei tre valori in uno unit test e l'intera classe di difetti da refuso sparisce
Dithering Floyd-Steinberg, e dove sta nella pipeline
La passata di dithering è un problema diverso dal resampling e vive in un punto diverso della pipeline. RenderOutputDither applica la diffusione dell'errore Floyd-Steinberg dopo la composizione della pagina, che è l'unica collocazione sensata per un'anteprima di stampa monocromatica o un export in stile fax: l'operazione riguarda la riduzione di un raster finito a un bit per pixel, non come le singole immagini sono state scalate all'ingresso
L'algoritmo in sé è corto. La luminanza viene sogliata al 50 percento, e l'errore di quantizzazione viene diffuso a quattro vicini con i classici pesi 7/16, 3/16, 5/16 e 1/16, verso destra, in basso a sinistra, in basso e in basso a destra. Il pixel di output è 0 o 255 su ogni canale. Ciò che dà invece l'alternativa ingenua, una soglia dura senza diffusione, trasforma una fotografia in una silhouette e perde ogni mezzitono che portava il contenuto
// Dithering al render per un dispositivo di anteprima monocromatico
Pdf.RenderOutputDither := True;
// Oppure applica la stessa passata a una bitmap che possiedi già. La bitmap
// dev'essere pf24bit; la funzione restituisce False invece di tirare a indovinare
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Accesso diretto al kernel quando ricampioni fuori dalla pipeline del documento
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
C'è un dettaglio implementativo nella diffusione dell'errore che morde tutti almeno una volta. Il buffer degli errori da riga a riga deve accumulare. Ogni pixel della riga successiva riceve contributi da tre pixel diversi della riga corrente, i tap 3/16, 5/16 e 1/16, e se il codice assegna invece di sommare, ogni scrittura butta via il contributo precedente e sopravvive solo l'ultimo tap. L'immagine sembra comunque ditherata, ed è questo che rende la cosa difficile da notare, ma la texture è sbagliata e la riproduzione tonale deriva. Il test che lo intercetta è quantitativo: fai il dithering di un campo grigio medio uniforme ed esigi che la copertura interna cada tra il 40 e il 60 percento
Quale combinazione deve usare una pipeline di riduzione dimensione?
Abbina il kernel a ciò che le immagini sono davvero, e tratta il dithering come una questione di dispositivo e non di compressione. Per le scansioni fotografiche che devono rispettare un budget di dimensione, rkLanczos3 a 150 o 200 DPI conserva il dettaglio che la gente nota tagliando il conteggio dei pixel di un fattore quattro o più. Per screenshot, diagrammi e line art, rkHalftone va più che bene ed è molto più veloce, perché quelle immagini hanno pochi gradienti tonali da preservare. Per un lotto misto in cui non puoi ispezionare ogni immagine, rkBicubic è la via di mezzo ragionevole: meglio del bilineare, all'incirca metà dei tap di Lanczos3
Il downsampling è una leva tra diverse, e non sempre è la più grande. Le scansioni bilevel di solito rispondono molto meglio all'encoder trattato in compressione JBIG2 bilevel nativa in Delphi, dove il guadagno viene dai dizionari di simboli e non dai conteggi di pixel. Prima di decidere aiuta sapere cosa c'è davvero nel file, ed è a ciò che serve estrarre le immagini e i loro decode filter: un inventario degli oggetti immagine e della loro compressione esistente ti dice se il resampling ha qualcosa da guadagnare
Se stai costruendo la superficie di anteprima che mostra il risultato, lo stesso percorso di rendering documentato in renderizzare una pagina PDF in una bitmap è dove RenderOutputDither fa effetto, così l'anteprima ditherata e l'output ditherato vengono da un solo percorso di codice invece che da due implementazioni che divergono
Il principio generale dietro entrambe le funzioni è che le impostazioni di qualità devono essere esplicite e reversibili. HotPDF conserva il comportamento storico come default così un'applicazione esistente si aggiorna senza una sorpresa nell'output o nei tempi, e mette i percorsi più belli e più lenti a una assegnazione di proprietà di distanza. Entrambi fanno parte del HotPDF Delphi PDF component, accanto alla macchina di ottimizzazione risorse e rendering su cui si basano