Articol tehnic

Downsampling de imagini și dithering de tipărire în HotPDF

HotPDF expune trei kernel-uri de downsampling a imaginilor prin proprietatea ImageDownsampleKernel și o trecere separată Floyd-Steinberg prin RenderOutputDither. Prima controlează cum arată fotografiile după ce le micșorați ca să încapă într-un buget de mărime, a doua controlează cum arată ele după ce o pagină e redusă la alb-negru. Niciuna nu e activă din start, iar ambele sunt opt-in din același motiv: costă timp real

Presiunea care aduce oamenii aici e cunoscută. Un contract scanat de 60 MB trebuie să iasă printr-un gateway de email care respinge orice peste 10 MB, sau un lot de extrase de cont trebuie să ajungă pe un dispozitiv monocrom în stil fax, care randează fiecare pixel gri fie ca hârtie, fie ca toner. Ambele probleme sunt probleme de resampling, iar ambele au un răspuns rapid, care arată rău, și un răspuns lent, care arată bine

În ce diferă de fapt cele trei kernel-uri

THPDFResampleKernel are trei valori, iar ele stau în puncte cu adevărat diferite pe curba viteză-calitate. rkHalftone delegă către calea istorică GDI StretchBlt cu modul HALFTONE, care, în ciuda numelui, e filtrare de clasă bilineară: rapidă, suficientă pentru line art și capturi de ecran și predispusă la muchiile tăioase pe care le recunoașteți instantaneu pe fotografiile micșorate. rkBicubic rulează un kernel separabil Catmull-Rom, iar rkLanczos3 rulează un sinc cu fereastră, separabil, cu un suport de trei lobi

Ambele kernel-uri separabile rulează în două treceri, orizontală apoi verticală, cu 6 până la 12 tap-uri per pixel destinație în Pascal pur. E cu un ordin de mărime mai lent aproximativ decât calea GDI, exact motivul pentru care rkHalftone rămâne implicit. Pe un lot nocturn de mii de pagini, diferența e o decizie de planificare, nu o preferință. Pe un document unic pe care un utilizator așteaptă, Lanczos3 e aproape gratis și vizibil mai bun

Curbele de greutate ale celor trei kernel-uri de downsampling HotPDF: rkHalftone delegă către calea GDI HALFTONE de clasă bilineară cu suport de unu, rkBicubic rulează un cubic separabil Catmull-Rom cu suport de doi, iar rkLanczos3 rulează un sinc cu fereastră și suport de trei, plătind cam un ordin de mărime din viteză pentru fotografii vizibil mai bune
Cele trei valori de kernel stau în puncte cu adevărat diferite pe curba viteză-calitate: o cale GDI de clasă bilineară, un cubic Catmull-Rom și un sinc cu fereastră de trei lobi, iar kernel-urile separabile normalizează greutățile, deci nimic nu sună dincolo de negru sau alb

Două proprietăți de implementare merită cunoscute pentru că determină ce poate și ce nu poate face rezultatul. Bordurile fac clamp prin replicarea muchiei, nu prin înfășurare sau estompare, iar greutățile sunt normalizate per pixel destinație. Împreună, cele două înseamnă că rezultatul nu sună niciodată sub negru sau peste alb, deci halo-ul clasic de overshoot Lanczos din jurul unei muchii dure nu apare ca artefacte tăiate în imaginea encodată

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // se setează înaintea apelului
    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;

Argumentul MinimumSavingsBytes, 4096 mai sus, e garda care ține operația cinstită. Reencodarea unei imagini deja comprimate eficient poate produce un stream mai mare decât originalul, iar un downsampler care înlocuiește orbește fiecare imagine va crește, o dată în eon, fișierul pe care i s-a cerut să îl micșoreze. Pragul spune: comiteți înlocuirea doar când economisește cel puțin atâția octeți. PreservedCalibratedImageCount raportează cealaltă decizie conservatoare, imaginile lăsate neatinse pentru că poartă un spațiu de culoare calibrat pe care resampling-ul l-ar compromite

De ce e atât de greu de observat un coeficient polinomial greșit?

Pentru că un kernel de interpolare stricat nu crapă și nu aruncă excepție, ci doar produce o imagine care arată subtil greșit într-un fel pe care nimeni nu îl poate atribui. Kernel-ul Catmull-Rom e cubic pe porțiuni, iar ramura lui exterioară în formă Horner imbricată e ((-0.5t + 2.5)t - 4)t + 2. Scrieți acel coeficient din mijloc ca -5 în loc de -4, iar funcția se evaluează în continuare, întoarce în continuare numere într-un interval plauzibil și produce în continuare o imagine

Defectul apare ca W(1) evaluând la -1 acolo unde trebuie să fie 0. Greutățile negative se acumulează, suma taie la zero, iar simptomul vizibil e un gradient al cărui capăt stâng devine negru și o muchie de treaptă care își pierde tonurile intermediare. Nimic din eșec nu arată spre un polinom. Verificarea care îl prinde în secunde e aritmetică, nu vizuală: un kernel de interpolare trebuie să satisfacă W(0) = 1 și W(±1) = W(±2) = 0, iar orice kernel care ratează cele trei puncte are o eroare de coeficient, punct. Afirmați asupra celor trei valori într-un test unitar și întreaga clasă de defecte de tip typo dispare

Graficul ramurii exterioare bicubic Catmull-Rom din HotPDF, arătând de ce se ascunde un coeficient greșit: greșeala de scriere care pune -5 în loc de -4 în forma Horner imbricată se evaluează în continuare și lasă W(1) la -1 și W(2) la -2 acolo unde e cerut zero, deci afirmarea W(0) egal 1 plus cele două constrângeri de zero îl prinde în secunde
Un kernel stricat nu crapă niciodată, doar întoarce numere care arată plauzibil, motiv pentru care ochiul nu poate prinde o greșeală de coeficient. W(0) = 1 cu zerouri la plus și minus unu și doi e un test unitar de trei linii

Dithering Floyd-Steinberg și locul lui în pipeline

Trecerea de dithering e o problemă diferită de resampling și stă într-un alt punct al pipeline-ului. RenderOutputDither aplică difuzia de eroare Floyd-Steinberg după compoziția paginii, singura plasare care are sens pentru o previzualizare de tipărire monocromă sau un export în stil fax: operația ține de reducerea unui raster finit la un bit per pixel, nu de modul în care au fost scalate imaginile individuale la intrare

Algoritmul în sine e scurt. Luminanța e praguită la 50 la sută, iar eroarea de cuantizare e difuzată spre patru vecini cu greutățile clasice 7/16, 3/16, 5/16 și 1/16, spre dreapta, stânga-jos, jos și dreapta-jos. Pixelul de ieșire e 0 sau 255 în fiecare canal. Varianta naivă, un prag dur fără difuzie, transformă o fotografie într-o siluetă și pierde fiecare ton intermediar care purta conținutul

Plasarea în pipeline a randării dithering-ului Floyd-Steinberg din HotPDF: RenderOutputDither rulează după compoziția paginii peste rasterul finit de 24 de biți, praguiește luminanța la 50 la sută și difuzează fiecare eroare de cuantizare spre dreapta și spre jos cu greutățile 7/16, 3/16, 5/16 și 1/16 printr-un buffer de rând care trebuie să acumuleze, producând ieșire monocromă de un bit
Dithering-ul aparține după compoziție pentru că reduce un raster finit la un bit, nu din cauza modului în care au fost scalate imaginile. Greutățile de difuzie însumează unu, iar buffer-ul de rând trebuie să acumuleze, nu să suprascrie
// Dithering la randare pentru un dispozitiv de previzualizare monocrom
Pdf.RenderOutputDither := True;

// Sau aplicați aceeași trecere unui bitmap pe care îl dețineți deja. Bitmap-ul
// trebuie să fie pf24bit; funcția returnează False în loc să ghicească
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// Acces direct la kernel când resample-ați în afara pipeline-ului documentului
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

Există un detaliu de implementare în difuzia de eroare care mușcă pe toată lumea o dată. Buffer-ul de eroare de la rând la rând trebuie să acumuleze. Fiecare pixel din rândul următor primește contribuții de la trei pixeli diferiți din rândul curent, tap-urile 3/16, 5/16 și 1/16, iar dacă codul atribuie în loc să adune, fiecare scriere aruncă contribuția anterioară și doar ultimul tap supraviețuiește. Imaginea încă pare dithered, iar asta o face greu de observat, dar textura e greșită și reproducția tonală derivează. Testul care îl prinde e cantitativ: aplicați dithering pe un câmp uniform de gri mediu și cereți acoperirii interioare să cadă între 40 și 60 la sută

Ce combinație ar trebui să folosească un pipeline de reducere de mărime?

Potriviți kernel-ul cu ce sunt efectiv imaginile și tratați dithering-ul ca o preocupare de dispozitiv, nu de compresie. Pentru scanări fotografice care trebuie să treacă printr-un buget de mărime, rkLanczos3 la 150 sau 200 DPI păstrează detaliul pe care oamenii îl observă, reducând numărul de pixeli de patru ori sau mai mult. Pentru capturi de ecran, diagrame și line art, rkHalftone e cu adevărat suficient și cu mult mai rapid, pentru că acele imagini au puține gradienți tonali de păstrat. Pentru un lot mixt în care nu puteți inspecta fiecare imagine, rkBicubic e mijlocul rezonabil: mai bun decât bilinear, cu cam jumătate din numărul de tap-uri al lui Lanczos3

Downsampling-ul e o pârghie dintre mai multe și nu e întotdeauna cea mai mare. Scanările bilevel răspund de regulă mult mai bine encoder-ului acoperit în compresia JBIG2 bilevel nativă în Delphi, unde câștigul vine din dicționarele de simboluri, nu din numărul de pixeli. Înainte să decideți, ajută să știți ce e efectiv în fișier, iar pentru asta e extragerea imaginilor și a filtrelor lor de decodare: un inventar al obiectelor imagine și al compresiei lor existente vă spune dacă resampling-ul are ceva de câștigat

Dacă construiți suprafața de previzualizare care arată rezultatul, aceeași cale de randare documentată în randarea unei pagini PDF într-un bitmap e locul unde RenderOutputDither își face efectul, deci previzualizarea dithered și ieșirea dithered vin dintr-o singură cale de cod, nu din două implementări care derivă una de la alta

Principiul larg din spatele ambelor trăsături e că setările de calitate ar trebui să fie explicite și reversibile. HotPDF păstrează comportamentul istoric ca implicit, astfel încât o aplicație existentă face upgrade fără o schimbare de surpriză în ieșire sau în timpi, și pune căile mai frumoase, dar mai lente, la o atribuire de proprietate distanță. Ambele fac parte din HotPDF Delphi PDF component, alături de mașinăria de optimizare a resurselor și de randare pe care se construiesc