Odborný článok

Adaptívne prevzorkovanie PDF obrázkov v Delphi s PDFiumPas

Po odoslaní kompresnej funkcie príde v priebehu týždňa dve sťažnosti: skenovaná zmluva má teraz schodovité, chlpaté tvary písmen a transparentné logo na obálke sedí v bledom hále. PDFiumPas odpovedá na obe na jednom mieste. TPdf.OptimizeImages zmeria každý obrázok skôr, než ho zmenší, potom vyberie jadro prevzorkovania a akumuluje farbu v podobe ohľaduplnej k alfa

Tak to nebolo vždy. Pred verziou v3.100.0 metóda prevzorkovala každý nebilevel obrázok pevným krokom najbližšieho suseda, čo je presne algoritmus, ktorý vyrobí obe sťažnosti: bodovo vzorkuje jeden zdrojový pixel na výstupný pixel a s RGB skrytým pod úplne priehľadným pixelom zaobchádza, akoby sa naň čitateľ kedy pozrel. Prepis vo v3.100.0 nahrádza túto jedinú cestu piatimi jadrami, meraným pravidlom výberu a explicitným rozpočtom pracovnej pamäte

Prečo pôsobí prevzorkovaný skenovaný text rozstrapkane?

Pretože bodové vzorkovanie odpovedá na nesprávnu otázku. Keď sa sken 300 DPI prerieši na 150 DPI, každý cieľový pixel zastupuje blok zdrojových pixelov dva krát dva a najbližší sused ponechá jeden zo štyroch a ostatné zahodí. Ktorý prežije, závisí od zaokrúhlenia, takže hrana ťahu plynulo vyhladená antialiasingom v zdroji sa stane hodom mince na pixel. Výsledok je klasické aliasované schodisko pozdĺž okrajov písmen a navyše moaré na rastrových plochách, kde zahodené vzorky práve niesli vzor. V PDF je to dôležitejšie než na obrazovke, pretože škoda je trvalá. Image XObject nesie svoje vzorkové dáta popri /Width, /Height a /BitsPerComponent (ISO 32000-1 §8.9.5) a prevzorkovanie prepíše všetky tri vnútri súboru. Zlé priblíženie v prehliadači je rámec, ktorý môžete prekresliť, a PDFiumPas na to má samostatnú mechaniku v render cache a výkone priblíženia. Zlé prevzorkovanie je nový dokument, ktorý odovzdáte zákazníkovi

Prečo prevzorkovanie najbližším susedom kazí skenovaný text v PDFiumPas pre Delphi: každý výstupný pixel ponechá jeden zo štyroch zdrojových pixelov a ostatné zahodí, čím vyrobí aliasované okraje písmen a moaré, čo päť jadier prevzorkovania nahrádza
Bodové vzorkovanie ponechá jeden zdrojový pixel na výstupný pixel a ostatné tri vyhodí, a preto PDFiumPas dnes ponúka päť jadier namiesto jedného

Ako PDFiumPas meria detail a vyberá jadro

PDFiumPas rozhoduje po obrázku, nie po dokumente. Skôr než vyberie jadro, vypočíta normalizované skóre detailu luminancie z ohraničenej vzorkovacej siete: horizontálne a vertikálne kroky sú (Width + 63) div 64 a (Height + 63) div 64, takže sken o 12000 pixeloch a miniatúra o 300 pixeloch stoja približne rovnaké prejdenie 64 krát 64. Na každej vzorkovanej pozícii sčíta absolútny rozdiel k susedovi vpravo a susedovi pod, cez až tri kanály, potom vydelí počtom vzoriek krát 255. Skóre sa usadí v rozsahu 0 až 1, kde plochá firemná grafika sedí blízko nuly a hustá fotografická textúra stúpa

Rebrík výberu potom beží v pevnom poradí. Ak je ResampleFilter čokoľvek iné než pirfAdaptive, používa sa tento filter doslova. Inak: 1-bitový obsah berie pirfBilevel; ContentClass piccLineArt berie pirfBox; faktor mierky 4 a viac berie tiež pirfBox, pretože pri takej redukcii je plošný priemer najlacnejšou aj najsprávnejšou odpoveďou; piccPhoto, skóre detailu 0.08 a viac alebo PreferredQuality 0.9 a viac berie pirfLanczos s jeho trojlobovým jadrom; mierka 2 a viac alebo kvalita 0.7 a viac berie pirfBicubic s polomerom 2; všetko ostatné berie pirfBilinear. Keďže TPdfImageOptimizeOptions.Default nastavuje PreferredQuality na 0.85, predvolený beh nikdy nespadne na bilineárny, pokiaľ redukcia nie je mierna a obsah plochý

Ako PDFiumPas vyberá jadro prevzorkovania v Delphi: ohraničené prejdenie 64 krát 64 vyrobí normalizované skóre detailu a potom pevný rebrík podmienok nasmeruje každý obrázok na filter bilevel, box, Lanczos, bikubický alebo bilineárny
Skóre detailu stojí na skene o 12000 pixeloch rovnako ako na miniatúre a rebrík pod ním sa zastaví pri prvej platnej podmienke
uses
  PDFium;

procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InputFile;
    // Predvolené: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
    // MinDimension 8, pirfAdaptive, piccAuto, kvalita 0.85, rozpočet 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;

Obrázok sa dotkne len vtedy, keď väčšia z hodnôt DPI jeho horizontálneho a vertikálneho umiestnenia delená TargetDpi dosiahne MinDpiRatio. Táto stráž existuje preto, aby fotografia 160 DPI mierená na cieľ 150 DPI nebola znovu enkódovaná pre šesťpercentný zisk, ktorý stojí jednu generáciu kvality. Obrázky pod MinDimension na ktorejkoľvek osi, predvolene 8, sa preskočia ako ikony alebo čiary

Prečo dostávajú transparentné logá biely okraj?

Pretože farba pod úplne priehľadným pixelom je ľubovoľná a obyčajný vážený priemer jej dovolí hlasovať. Vyexportujete logo z dizajnérskeho nástroja a neviditeľný okraj je často biely, čierny alebo čokoľvek, čím bolo plátno; alfa kanál ho schová a priamy súčet cez stopu jadra ho rýchlo zamieša späť do viditeľného okraja. PDFiumPas sa tomu vyhýba tak, že akumuluje vzorky BGRA v premultiplikovanej podobe a premultiplikáciu odvolá až na cieľovom pixeli

Konkrétne: každá prispievajúca vzorka pridá channel * alpha * weight do akumulátora farby, alpha * weight do akumulátora alfa a weight do súčtu váh. Cieľová farba sa potom vydelí akumulátorom alfa, nie súčtom váh, a presne v tom je krok, ktorý rozhoduje: delenie súčtom váh by farbu ťahalo k neviditeľným pixelom, zatiaľ čo delenie akumulovanou alfou zrekonštruuje farbu, na ktorej sa viditeľné vzorky skutočne zhodli. Cieľová alfa je samostatná veličina, 255 * AlphaSum / WeightSum. Formáty bez alfy delia súčtom váh ako zvyčajne, výplňový bajt cieľa FPDFBitmap_BGRx sa zapíše ako konštantných 255 a každý kanál sa pred uložením pritlačí do rozsahu 0 až 255. Táto alfa zvykne pochádzať z položky soft mask v slovníku obrázku (ISO 32000-1 §11.4), ktorú PDFium už skompozitoval do vyrovnávacej pamäte BGRA, ktorú resampler dostáva

Ako PDFiumPas odstraňuje biely halo z transparentných PDF obrázkov v Delphi: vzorky sa akumulujú v premultiplikovanej podobe a cieľová farba sa delí akumulovanou alfou namiesto súčtu váh, takže neviditeľné pixeli nemôžu hlasovať
Delenie premultiplikovanej farby akumulovanou alfou zrekonštruuje to, na čom sa viditeľné vzorky zhodli, zatiaľ čo delenie súčtom váh ťahá okraj k neviditeľným pixelom
// Tvar vnútornej akumulačnej slučky, pre každú prispievajúcu zdrojovú vzorku
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;

// ... a v cieľovom pixeli, odpremultiplovať proti súčtu 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;

Ako držať 1-bitovú linkovú grafiku mimo šedej zóny

Každé spojité jadro aplikované na bilevel sken vyrobí sivú a sivá je presne to, čo faxový štýl obrázku obsahovať nesmie. PDFiumPas preto 1-bitové obrázky v predvolenom stave necháva tak, ako sú: PreserveBilevel je True v TPdfImageOptimizeOptions.Default a také obrázky skončia v SkippedCount nedotknuté. Nastavte na False a cestu prevezme pirfBilevel namiesto vyhladzovacieho jadra. Tá prejde presný zdrojový obdĺžnik pokrývajúci každý cieľový pixel, spriemeruje luminanciu s váhami 0.114, 0.587 a 0.299 v poradí pamäte BGR a výsledok prahuje pri 127.5 na ploché 0 alebo 255. Nič medziľahlé sa zapísať nedá, takže okraje zostávajú ostré a okolo tenkých ťahov nevzniká sivý halo; alfa kanál BGRA zdroja sa spriemeruje normálne a cieľ BGRx dostane konštantných 255. Ak potrebujete podkladové pixeli a nie menší dokument, extrakcia obrázkov z PDF dokumentov je samostatná cesta

Čo sa stane, keď obrázok prekročí rozpočet pracovnej pamäte?

Nechá sa presne tak, ako bol, a započíta sa. MaxWorkingBytes má predvolených 64 MiB a vynucuje sa dvakrát. Pred vytvorením cieľovej bitmapy PDFiumPas obrázok odmietne, ak šírka krát výška krát bajtov na pixel prekročí rozpočet. Po úspešnom FPDFBitmap_CreateEx skontroluje znovu pomocou skutočného stride krát výšky, pretože výplň riadkov môže alokáciu zatlačiť za limit, ktorý naivný súčin pustil. Každé odmietnutie zničí cieľ a nevráti nič. Buďte si vedomí degradácie, ktorú to znamená: obrázok nad rozpočet sa neprevzorkuje v nižšej kvalite a nedelí sa na dlaždice. Originál zostáva v dokumente, BudgetExceededCount aj SkippedCount stúpnu a beh preto môže nahlásiť úspech, zatiaľ čo dokument je optimalizovaný len čiastočne. Toto je zámerné fail-safe správanie, ale znamená to, že report nie je nepovinné čítanie. Existuje aj osobitný režim zlyhania: obrázky, ktorých bitmapu PDFium nedokáže vyrobiť vôbec, ako CMYK, JPX, JBIG2 alebo maskované zdroje, zvýšia namiesto toho FailedCount a zostanú rovnako nedotknuté

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // použiť plošné hlasovanie bilevel
  Options.ContentClass := piccPhoto;             // vynútiť Lanczos pre fotografické sady
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // rezerva pre veľké skeny
  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;

Čítanie reportu skôr, než súbor odošlete

TPdfImageOptimizeReport je stavaný na diagnostiku, nielen na zalogovanie. Popri OptimizedCount, SkippedCount a FailedCount vystavuje jedno počítadlo na jadro, takže BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount a BilevelFilterCount vám povedia, k akému záveru o vašom korpuse adaptívne pravidlo skutočne dospelo. Výsledok všetko-box znamená, že redukcie boli prudké alebo obsah bol klasifikovaný ako linková grafika; výsledok všetko-Lanczos na dokumente, o ktorom ste verili, že je linková grafika, je známkou, že ContentClass by sa mal nastaviť explicitne. AverageDetailScore je číslo na porovnanie s prahom 0.08 pre Lanczos pri ladení PreferredQuality a PeakWorkingBytes ukazuje, koľko z MaxWorkingBytes beh skutočne potreboval. Neplatné voľby zlyhajú nahlas, nie potichu: nekladné TargetDpi, MinDpiRatio pod 1, PreferredQuality mimo rozsahu 0 až 1 alebo nekladné MaxWorkingBytes vyvolajú EPdfError skôr, než sa čokoľvek dotkne akejkoľvek stránky. A OptimizeImages upravuje len dokument v pamäti; každá upravená stránka sa zaviaže cez FPDFPage_GenerateContent, po čom SaveAs stále voláte sami. Ak chcete na vlastné oči vidieť, čo sa zmenilo, vyrenderujte dokumenty pred a po na bitmapy ako popisuje konverzia stránok PDF na JPEG obrázky a porovnajte ich pri plnom priblížení

Adaptívne prevzorkovanie patrí k funkciám, ktoré sú neviditeľné, keď fungujú, a generujú support tickety, keď nefungujú; preto meranie, správa alfy a rozpočet pamäte museli prísť spolu, nie ako tri oddelené vylepšenia. Ak to vyhodnocujete pre produkt v Delphi, C++Builder alebo Lazare, úplný povrch API a licenčné detaily nájdete na stránke PDFiumPas Delphi PDFium component