Articol tehnic

Resampling adaptiv al imaginilor PDF în Delphi cu PDFiumPas

Două plângeri sosesc în săptămâna de după lansarea unei funcții de comprimare: contractul scanat are acum litere zimțate și lânoase, iar logo-ul transparent de pe copertă stă într-o aureolă palidă. PDFiumPas le răspunde pe amândouă într-un singur loc. TPdf.OptimizeImages măsoară fiecare imagine înainte s-o micșoreze, apoi alege un nucleu de resampling și acumulează culoarea ținând cont de alfa

Nu a fost mereu așa. Înainte de v3.100.0, aceeași metodă reducea fiecare imagine non-bilevel cu un pas fix de nearest-neighbour, exact algoritmul care produce ambele plângeri: preleva un singur pixel sursă per pixel de ieșire și trata RGB-ul aflat sub un pixel complet transparent ca și cum un cititor l-ar vedea vreodată. Rescrierea din v3.100.0 înlocuiește acel drum unic cu cinci nuclee, o regulă de selecție măsurată și un buget explicit de memorie de lucru

De ce face reducerea textul scanat să pară zimțat?

Pentru că prelevarea punctuală răspunde la întrebarea greșită. Când o scanare de 300 DPI e rețintită la 150 DPI, fiecare pixel de destinație reprezintă un bloc de doi pe doi de pixeli sursă, iar nearest neighbor păstrează unul din cei patru și aruncă restul. Care anume supraviețuiește depinde de rotunjire, deci o muchie de trasă netedă prin antialiasing în sursă devine o aruncare cu banul per pixel. Rezultatul e scara clasică aliasing de-a lungul marginilor glifelor, plus moire în zonele de raster unde mostrele aruncate purtau din întâmplare tiparul. Contează mai mult într-un PDF decât pe ecran pentru că dauna e permanentă. Un image XObject își poartă datele de mostră alături de /Width, /Height și /BitsPerComponent (ISO 32000-1 §8.9.5), iar resampling-ul rescrie toate trei în interiorul fișierului. Un zoom prost într-un viewer e un cadru pe care îl puteți redesena, iar PDFiumPas are utilaje separate pentru asta în render cache și performanță zoom. Un downsample prost e un document nou pe care îl predați clientului

De ce nearest-neighbour ruiniază textul scanat în PDFiumPas pentru Delphi: fiecare pixel de ieșire păstrează unul din patru pixeli sursă și aruncă restul, producând margini de glife aliasing și moire, ceea ce cele cinci nuclee de resampling înlocuiesc
Prelevarea punctuală păstrează un pixel sursă per pixel de ieșire și aruncă celelalte trei, de aceea PDFiumPas oferă acum cinci nuclee în locul unuia

Cum măsoară PDFiumPas detaliul și alege un nucleu

PDFiumPas decide per imagine, nu per document. Înainte să aleagă un nucleu calculează un scor normalizat de detaliu al luminanței dintr-o grilă de eșantionare mărginită: pașii orizontal și vertical sunt (Width + 63) div 64 și (Height + 63) div 64, deci o scanare de 12000 pixeli și o miniatură de 300 pixeli costă cam același baleiaj de 64 pe 64. La fiecare poziție eșantionată adună diferența absolută față de vecinul din dreapta și vecinul de dedesubt, pe până la trei canale, apoi împarte la numărul de mostre ori 255. Scorul cade între 0 și 1, unde grafica de afaceri plată stă lângă zero, iar textura fotografică densă urcă

Scara de selecție rulează apoi într-o ordine fixă. Dacă ResampleFilter e altceva decât pirfAdaptive, acel filtru e folosit ca atare. Altfel: conținutul de 1 bit ia pirfBilevel; un ContentClass de piccLineArt ia pirfBox; un factor de scală de 4 sau mai mult ia la fel pirfBox, pentru că la reducerea aceea o medie de arie e totodată răspunsul cel mai ieftin și cel mai corect; piccPhoto, un scor de detaliu de 0.08 sau mai mare, sau un PreferredQuality de 0.9 sau mai mare ia pirfLanczos cu nucleul său cu trei lobi; o scală de 2 sau mai mult sau o calitate de 0.7 sau mai mare ia pirfBicubic la raza 2; tot ce rămâne ia pirfBilinear. Deoarece TPdfImageOptimizeOptions.Default setează PreferredQuality la 0.85, o rulare implicită nu cade niciodată pe bilinear decât dacă reducerea e blândă și conținutul plat

Cum alege PDFiumPas un nucleu de resampling în Delphi: un baleiaj mărginit de 64 pe 64 produce un scor normalizat de detaliu, apoi o scară fixă de condiții trimite fiecare imagine la filtrul bilevel, box, Lanczos, bicubic sau bilinear
Scorul de detaliu costă la fel pe o scanare de 12000 pixeli ca pe o miniatură, iar scara de sub el se oprește la prima condiție care se potrivește
uses
  PDFium;

procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InputFile;
    // Valori implicite: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
    // MinDimension 8, pirfAdaptive, piccAuto, calitate 0.85, buget 64 MiB.
    Options := TPdfImageOptimizeOptions.Default;
    Options.TargetDpi := 150;
    Options.MinDpiRatio := 1.5;
    Options.ContentClass := piccAuto;
    Options.PreferredQuality := 0.85;
    if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
      Pdf.SaveAs(OutputFile);
  finally
    Pdf.Free;
  end;
end;

O imagine e atinsă doar când cel mai mare dintre DPI-urile sale de plasament orizontal și vertical, împărțit la TargetDpi, atinge MinDpiRatio. Garda aceea există ca o fotografie de 160 DPI țintită la un țel de 150 DPI să nu fie recodificată pentru un câștig de șase procente care costă o generație de calitate. Imaginile sub MinDimension pe oricare axă, 8 implicit, sunt omise ca icoane sau linii

De ce capătă logo-urile transparente o franjură albă?

Pentru că culoarea de sub un pixel complet transparent e arbitrară, iar o medie ponderată obișnuită o lasă să voteze. Exportați un logo dintr-un instrument de design și marginea invizibilă e frecvent albă, sau neagră, orice fusese planșeta; canalul alfa o ascunde, iar o sumă directă peste amprenta nucleului o amestecă prompt înapoi în marginea vizibilă. PDFiumPas evită asta acumulând mostre BGRA în formă premultiplicată și anulând premultiplicarea doar la pixelul de destinație

Concret, fiecare mostră contribuitoare adună channel * alpha * weight în acumulatorul de culoare, alpha * weight într-un acumulator de alfa, și weight în suma ponderilor. Culoarea de destinație e apoi împărțită la acumulatorul de alfa, nu la suma ponderilor, și aceea e pasul care contează: împărțirea la suma ponderilor ar târî culoarea spre pixelii invizibili, pe când împărțirea la alfa acumulat reconstruiește culoarea asupra căreia mostrele vizibile au căzut de acord de fapt. Alfa de destinație e o mărime separată, 255 * AlphaSum / WeightSum. Formatele fără alfa împart la suma ponderilor ca de obicei, octetul de umplutură al unei destinații FPDFBitmap_BGRx e scris ca constanta 255, iar fiecare canal e limitat între 0 și 255 înainte de a fi stocat. Alfa respectivă provine de regulă dintr-o intrare de soft mask în dicționarul imaginii (ISO 32000-1 §11.4), pe care PDFium a compus-o deja în bufferul BGRA pe care îl primește resampler-ul

Cum elimină PDFiumPas aureola albă din imaginile PDF transparente în Delphi: mostrele sunt acumulate în formă premultiplicată, iar culoarea de destinație e împărțită la alfa acumulat în locul sumei ponderilor, ca pixelii invizibili să nu poată vota
Împărțirea culorii premultiplicate la alfa acumulat reconstruiește la ce au căzut de acord mostrele vizibile, pe când împărțirea la suma ponderilor târăște marginea spre pixelii invizibili
// Forma buclei interioare de acumulare, per mostră sursă contribuitoare
if SrcFormat = FPDFBitmap_BGRA then
  Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
  Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
  Accumulated[Channel] := Accumulated[Channel] +
    PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;

// ... iar la pixelul de destinație, anulează premultiplicarea cu suma de alfa
if SrcFormat = FPDFBitmap_BGRA then
begin
  if Abs(AlphaSum) > 1E-12 then
    ValueSum := Accumulated[Channel] / AlphaSum
  else
    ValueSum := 0;
end
else
  ValueSum := Accumulated[Channel] / WeightSum;

Ținerea artei liniare de 1 bit în afara zonei gri

Orice nucleu continuu aplicat unei scanări bilevel produce gri, iar griul e exact ce unei imagini în stil fax nu-i este permis să conțină. PDFiumPas lasă prin urmare imaginile de 1 bit în pace implicit: PreserveBilevel e True în TPdfImageOptimizeOptions.Default, iar asemenea imagini ajung în SkippedCount neatinse. Setați-l pe False și drumul pirfBilevel preia controlul în locul unui nucleu de netezire. El parcurge dreptunghiul sursă exact care acoperă fiecare pixel de destinație, mediează luminozitatea cu ponderile 0.114, 0.587 și 0.299 în ordinea de memorie BGR, și praghește rezultatul la 127.5 într-un 0 sau 255 plat. Nimic intermediar nu poate fi scris, deci muchiile rămân crisp și nicio aureolă gri nu se formează în jurul traselor subțiri; canalul alfa al unei surse BGRA e mediat normal, iar o destinație BGRx primește constanta 255. Dacă aveți nevoie de pixelii de dedesubt mai degrabă decât de un document mai mic, extragerea imaginilor din documente PDF e drumul separat

Ce se întâmplă când o imagine depășește bugetul de memorie de lucru?

E lăsată exact cum era și e numărată. MaxWorkingBytes e implicit 64 MiB și e aplicat de două ori. Înainte ca bitmap-ul de destinație să fie creat, PDFiumPas respinge imaginea dacă lățime ori înălțime ori bytes per pixel depășește bugetul. După ce FPDFBitmap_CreateEx reușește verifică din nou folosind stride-ul real ori înălțimea, pentru că umplutura de rând poate împinge o alocare peste o limită pe care produsul naiv o lăsa să treacă. Oricare respingere distruge destinația și nu întoarce nimic. Fiți limpede cu privire la degradarea asta: o imagine peste buget nu e reeșantionată la o calitate mai joasă și nu e tăiată în dale. Originalul rămâne în document, BudgetExceededCount și SkippedCount cresc ambele, iar o rulare poate deci raporta succes în timp ce un document e doar parțial optimizat. E comportament fail-safe deliberat, dar înseamnă că raportul nu e o lectură opțională. Există și un mod de eșec distinct: imaginile a căror bitmap PDFium nu o poate produce deloc, precum surse CMYK, JPX, JBIG2 sau mascate, cresc în schimb FailedCount și sunt la fel lăsate neatinse

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // folosește votul de arie bilevel
  Options.ContentClass := piccPhoto;             // forțează Lanczos pentru seturile foto
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // loc liber pentru scanări mari
  Pdf := TPdf.Create(nil);
  try
    for I := Low(Files) to High(Files) do
    begin
      Pdf.FileName := Files[I];
      if not Pdf.OptimizeImages(Options, Report) then
      begin
        WriteLn('optimize failed: ', Report.ErrorMessage);
        Continue;
      end;
      if Report.BudgetExceededCount > 0 then
        WriteLn(Files[I], ': ', Report.BudgetExceededCount,
          ' image(s) over budget and kept at full size');
      if Report.FailedCount > 0 then
        WriteLn(Files[I], ': ', Report.FailedCount,
          ' image(s) could not be decoded to a bitmap');
      if Report.OptimizedCount > 0 then
        Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
    end;
  finally
    Pdf.Free;
  end;
end;

Citirea raportului înainte să expediați fișierul

TPdfImageOptimizeReport e construit să fie diagnosticat, nu doar logat. Lângă OptimizedCount, SkippedCount și FailedCount expune un contor per nucleu, deci BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount și BilevelFilterCount vă spun ce a concluzionat de fapt regula adaptivă despre corpusul dumneavoastră. Un rezultat tot-box înseamnă că reducerile au fost abrupte sau conținutul a fost clasificat ca artă liniară; un rezultat tot-Lanczos pe un document pe care credeați că e artă liniară e semn că ContentClass ar trebui setat explicit. AverageDetailScore e numărul de comparat cu pragul Lanczos de 0.08 când ajustați PreferredQuality, iar PeakWorkingBytes arată cât din MaxWorkingBytes a avut nevoie rularea de fapt. Opțiunile invalide eșuează zgomotos, nu tăcut: un TargetDpi non-pozitiv, un MinDpiRatio sub 1, un PreferredQuality în afara intervalului 0–1, sau un MaxWorkingBytes non-pozitiv ridică EPdfError înainte ca vreo pagină să fie atinsă. Iar OptimizeImages editează doar documentul din memorie; fiecare pagină modificată e comisă cu FPDFPage_GenerateContent, după care tot dumneavoastră apelați SaveAs. Ca să vedeți cu ochiul ce s-a schimbat, randați documentele dinainte și de după în bitmap-uri așa cum e descris în conversia paginilor PDF în imagini JPEG și comparați-le la zoom maxim

Resampling-ul adaptiv e una dintre funcțiile acelea invizibile când merg și care generează tichete de suport când nu merg, de asta măsurarea, tratarea alfa și bugetul de memorie au trebuit să ajungă împreună, nu ca trei rafinări separate. Dacă evaluați asta pentru un produs Delphi, C++Builder sau Lazarus, suprafața completă de API și detaliile de licențiere sunt pe pagina componentei PDFiumPas Delphi PDFium