Articolo tecnico

Kernel di downsampling e dithering di stampa in HotPDF

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

Curve dei pesi dei tre kernel di downsampling di HotPDF: rkHalftone delega al percorso GDI HALFTONE di classe bilineare con supporto uno, rkBicubic esegue una cubica Catmull-Rom separabile con supporto due, e rkLanczos3 esegue un sinc finestrato con supporto tre, scambiando all'incirca un ordine di grandezza di velocità per fotografie visibilmente migliori
I tre valori di kernel stanno in punti genuinamente diversi della curva velocità-qualità: un percorso GDI di classe bilineare, una cubica Catmull-Rom e un sinc finestrato a tre lobi, e i kernel separabili normalizzano i pesi così niente suona oltre il nero o il bianco

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

Grafico del ramo esterno bicubico Catmull-Rom di HotPDF che mostra perché un coefficiente sbagliato si nasconde: il refuso che scrive -5 invece di -4 nella forma di Horner annidata valuta ancora e lascia W(1) a -1 e W(2) a -2 dove serve zero, quindi asserire che W(0) vale 1 più i due vincoli a zero lo intercetta in pochi secondi
Un kernel rotto non va mai in crash, restituisce solo numeri che sembrano plausibili, ed è per questo che l'occhio non intercetta un refuso di coefficiente. W(0) = 1 con zeri a più e meno uno e due è uno unit test di tre righe

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

Collocazione nella pipeline di render del dithering Floyd-Steinberg di HotPDF: RenderOutputDither gira dopo la composizione della pagina sul raster a 24 bit finito, soglia la luminanza al 50 percento e diffonde ogni errore di quantizzazione a destra e in basso con pesi 7/16, 3/16, 5/16 e 1/16 attraverso un buffer di riga che deve accumulare, producendo output monocromatico a un bit
Il dithering sta dopo la composizione perché riduce a un bit un raster finito, non per come le immagini sono state scalate. I pesi di diffusione sommano a uno, e il buffer di riga deve accumulare e non sovrascrivere
// 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