Technický článek

Adaptivní převzorkování obrázků PDF v Delphi s PDFiumPas

Těsně po vydání kompresní funkci přijdou dvě stížnosti: naskenovaná smlouva má teď schodovité, chlupaté letterformy a průhledné logo na titulní stránce sedí uvnitř bledé svatozáře. PDFiumPas odpovídá na obojí na jednom místě. TPdf.OptimizeImages změří každý obrázek, než jej zmenší, pak zvolí jádro převzorkování a akumuluje barvu ve formě citlivé na alfu

To neplatilo vždy. Před v3.100.0 stejná metoda zmenšovala každý ne bilevel obrázek pevným krokem nejbližšího souseda, což je přesně algoritmus, který vytváří obě stížnosti: vzorkuje bod po jednom zdrojovém pixelu na výstupní pixel a s RGB sedícím pod zcela průhledným pixelem zachází, jako by ho čtenář kdy viděl. Přepis ve v3.100.0 nahrazuje tu jedinou cestu pěti jádry, měřeným pravidlem výběru a explicitním rozpočtem pracovní paměti

Proč zmenšování rozcuchá naskenovaný text?

Protože bodové vzorkování odpovídá na špatnou otázku. Když je sken 300 DPI přeciílen na 150 DPI, každý cílový pixel zastupuje blok zdrojových pixelů dva krát dva a nejbližší soused podrží jeden ze čtyř a zahodí zbytek. Který přežije, závisí na zaokrouhlování, takže hrana tahu, která byla ve zdroji hladce antialiasovaná, se stane hodem mincí na pixel. Výsledkem je klasické schodovité aliasování podél hran glyfů plus moaré na rastru, kde zahodené vzorky náhodou nesly vzor. V PDF to zasahuje víc než na obrazovce, protože škoda je trvalá. Image XObject nese vzorová data vedle /Width, /Height a /BitsPerComponent (ISO 32000-1 §8.9.5) a převzorkování přepíše všechna tři uvnitř souboru. Špatný zoom v prohlížeči je snímek, který můžete překreslit, a PDFiumPas má na to samostatné stroje v článku render cache a zoom performance. Špatné zmenšení je nový dokument, který podáte zákazníkovi

Proč zmenšování nejbližším sousedem ničí naskenovaný text v PDFiumPas pro Delphi: každý výstupní pixel podrží jeden ze čtyř zdrojových pixelů a zahodí zbytek, čímž vytváří aliasované hrany glyfů a moaré, což pět jader převzorkování nahrazuje
Bodové vzorkování podrží jeden zdrojový pixel na výstupní pixel a tři ostatní zahodí, proto PDFiumPas dnes nabízí pět jader místo jednoho

Jak PDFiumPas měří detail a volí jádro

PDFiumPas rozhoduje na obrázek, ne na dokument. Před volbou jádra vypočte normalizované skóre detailu jasu z ohraničené vzorkovací mřížky: vodorovné a svislé kroky jsou (Width + 63) div 64 a (Height + 63) div 64, takže sken o 12000 pixelech a miniatura o 300 pixelech stojí zhruba totéž zametání 64 krát 64. Na každé vzorkované pozici sčítá absolutní rozdíl k sousedovi vpravo a sousedovi níže, přes až tři kanály, pak dělí počtem vzorků krát 255. Skóre spadá do 0 až 1, kde plochá firemní grafika sedí poblíž nuly a hustá fotografická textura stoupá

Výběrový žebříček pak běží v pevném pořadí. Není-li ResampleFilter ničím jiným než pirfAdaptive, použije se ten filtr doslova. Jinak: 1bitový obsah bere pirfBilevel; ContentClass piccLineArt bere pirfBox; měřítko 4 a více bere též pirfBox, protože při tom zmenšení je plošný průměr jak nejlevnější, tak nejpravdivější odpověď; piccPhoto, skóre detailu 0.08 a více, nebo PreferredQuality 0.9 a více bere pirfLanczos s jeho třílaločným jádrem; měřítko 2 a více nebo kvalita 0.7 a více bere pirfBicubic o poloměru 2; všechno zbylé bere pirfBilinear. Protože TPdfImageOptimizeOptions.Default nastavuje PreferredQuality na 0.85, výchozí běh nikdy nespadne na bilineární, ledaže je zmenšení mírné a obsah plochý

Jak PDFiumPas volí jádro převzorkování v Delphi: ohraničené zametání 64 krát 64 vytvoří normalizované skóre detailu, pak pevný žebříček podmínek směruje každý obrázek na filtr bilevel, box, Lanczos, bikubický nebo bilineární
Skóre detailu stojí na skenu o 12000 pixelech totéž co na miniatuře a žebříček pod ním se zastaví na první podmínce, která sedí
uses
  PDFium;

procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InputFile;
    // Výchozí: 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;

Na obrázek se sáhne jen tehdy, když větší z jeho vodorovného a svislého umístovacího DPI, dělené TargetDpi, dosáhne MinDpiRatio. Ta stráž existuje proto, aby fotografie 160 DPI mířená na cíl 150 DPI nebyla znovu zakódována pro šestiprocentní zisk, který stojí generaci kvality. Obrázky pod MinDimension na kterékoliv ose, 8 ve výchozím stavu, se přeskočí jako ikony nebo linky

Proč průhledná loga sbírají bílý lem?

Protože barva pod zcela průhledným pixelem je libovolná a obyčejný vážený průměr jí dovolí hlasovat. Vyexportujte logo z designového nástroje a neviditelný okraj bývá často bílý, nebo černý, nebo cokoliv, čím plátno bylo; kanál alfa jej skrývá a přímý součet po stopě jádra jej neprodleně zpětně zamíchá do viditelné hrany. PDFiumPas se tomu vyhne akumulováním vzorků BGRA v premultiplikované formě a zrušením premultiplikace až na cílovém pixelu

Konkrétně každý přispívající vzorek přidá channel * alpha * weight do akumulátoru barvy, alpha * weight do akumulátoru alfy a weight do součtu vah. Cílová barva se pak dělí akumulátorem alfy, nikoli součtem vah, a to je krok, který rozhoduje: dělení součtem vah by vleklo barvu k neviditelným pixelům, zatímco dělení akumulovanou alfou rekonstruuje barvu, na které se viditelné vzorky skutečně shodly. Cílová alfa je oddělená veličina, 255 * AlphaSum / WeightSum. Formáty bez alfy dělí součtem vah jako obvykle, výplňový bajt cíle FPDFBitmap_BGRx se zapíše jako konstantní 255 a každý kanál se před uložením sevře do 0 až 255. Ta alfa normálně pochází z položky soft masky ve slovníku obrázku (ISO 32000-1 §11.4), kterou PDFium už zkombinoval do bufferu BGRA, který převzorkovač přijímá

Jak PDFiumPas odstraňuje bílou svatozář z průhledných obrázků PDF v Delphi: vzorky se akumulují v premultiplikované formě a cílová barva se dělí akumulovanou alfou místo součtu vah, takže neviditelné pixely nemohou hlasovat
Dělení premultiplikované barvy akumulovanou alfou rekonstruuje, na čem se viditelné vzorky shodly, zatímco dělení součtem vah vleče hranu k neviditelným pixelům
// Tvar vnitřní akumulační smyčky, na přispívající zdrojový vzorek
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 na cílovém pixelu, zpětná premultiplikace vůči součtu alfy
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;

Jak držet 1bitovou line art mimo šedou zónu

Každé spojité jádro aplikované na bilevel sken vytvoří šedou a šedá je přesně to, co faxový obrázek nesmí obsahovat. PDFiumPas proto nechává 1bitové obrázky ve výchozím stavu beze všeho: PreserveBilevel je True v TPdfImageOptimizeOptions.Default a takové obrázky dopadnou do SkippedCount nedotčeny. Nastavte False a cestu pirfBilevel převezme místo jádra vyhlazování. Projde přesný zdrojový obdélník pokrývající každý cílový pixel, zprůměruje jas s váhami 0.114, 0.587 a 0.299 v paměťovém pořadí BGR a prahuje výsledek na 127.5 do plochého 0 nebo 255. Nic mezi tím nelze zapsat, takže hrany zůstávají ostré a kolem tenkých tahů nevzniká šedá svatozář; kanál alfy zdroje BGRA se zprůměruje normálně a cíl BGRx dostane konstantní 255. Pokud potřebujete podkladové pixely, ne menší dokument, extrakce obrázků z dokumentů PDF je samostatná cesta

Co se stane, když obrázek překročí rozpočet pracovní paměti?

Zůstane přesně takový, jaký byl, a započítá se. MaxWorkingBytes má výchozí 64 MiB a vynucuje se dvakrát. Před vytvořením cílové bitmapy PDFiumPas obrázek odmítne, pokud šířka krát výška krát bajty na pixel přesáhne rozpočet. Po úspěchu FPDFBitmap_CreateEx zkontroluje znovu pomocí skutečného stride krát výška, protože výplň řádků dokáže zatlačit alokaci přes limit, který naivní součin pustil. Kterékoliv odmítnutí zničí cíl a nevrátí nic. Buďte jasní v degradaci, kterou to znamená: obrázek nad rozpočet se nepřevzorkuje nižší kvalitou a nerozdělí se na dlaždice. Originál zůstane v dokumentu, BudgetExceededCount i SkippedCount vzrostou a běh tak může ohlásit úspěch, zatímco dokument je optimalizován jen částečně. To je záměrné fail-safe chování, ale znamená to, že report není čtení na volitelnost. Existuje též odlišný režim selhání: obrázky, jejichž bitmapu PDFium nedokáže vůbec vytvořit, jako CMYK, JPX, JBIG2 nebo maskované zdroje, zvětší místo toho FailedCount a rovněž zůstanou nedotčeny

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // použij plošný hlas bilevel
  Options.ContentClass := piccPhoto;             // vynuť Lanczos pro fotografické sady
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // rezerva pro velké 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;

Čtěte report, než soubor odešlete

TPdfImageOptimizeReport je stavěn k diagnóze, ne jen k logování. Vedle OptimizedCount, SkippedCount a FailedCount vystavuje jedno počítadlo na jádro, takže BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount a BilevelFilterCount vám řeknou, k jakému závěru adaptivní pravidlo o vašem korpusu skutečně došlo. Výsledek všeho box znamená, že zmenšení byla prudká nebo obsah byl klasifikován jako line art; výsledek všeho Lanczos na dokumentu, o kterém jste věřili, že je line art, je znamení, že ContentClass by měl být nastaven explicitně. AverageDetailScore je číslo k porovnání s prahem Lanczos 0.08 při ladění PreferredQuality a PeakWorkingBytes ukazuje, kolik z MaxWorkingBytes běh skutečně potřeboval. Neplatné volby selhávají nahlas, nikoli potichu: nekladné TargetDpi, MinDpiRatio pod 1, PreferredQuality mimo 0 až 1, nebo nekladné MaxWorkingBytes vyvolá EPdfError, dřív než je dotčena kterákoliv stránka. A OptimizeImages upravuje jen dokument v paměti; každá upravená stránka se potvrdí přes FPDFPage_GenerateContent, po čemž ještě sami zavoláte SaveAs. K zření toho, co se změnilo, vyrenderujte dokumenty před a po do bitmap podle převodu stránek PDF na obrázky JPEG a porovnejte je při plném zoomu

Adaptivní převzorkování je jedna z těch funkcí, které jsou neviditelné, když fungují, a generují support tickety, když nefungují, proto měření, zpracování alfy a rozpočet paměti musely dopadnout společně, nikoli jako tři oddělená vylepšení. Pokud to vyhodnocujete pro produkt Delphi, C++Builder nebo Lazarus, úplný povrch API a detaily licencování jsou na stránce PDFiumPas Delphi PDFium component