Tehnički članak

Adaptivno preuzorkovanje PDF slika u Delphi-ju

Dve žalbe stižu nedelju dana posle nego što funkcija kompresije isporuči: skenirani ugovor sada ima stepenasta, dlakava slova, i transparentni logo na naslovnoj stranici sedi unutar bledog oreola. PDFiumPas odgovara na obe na jednom mestu. TPdf.OptimizeImages meri svaku sliku pre nego što je smanji, pa bira kernel za preuzorkovanje i nakuplja boju u alpha svesnom obliku

To nije uvek bilo tako. Pre v3.100.0 ista metoda je smanjivala svaku ne-bilevel sliku fiksnim najbliži-sused korakom, što je baš algoritam koji proizvodi obe žalbe: tačkastim uzorkovanjem uzima jedan izvorni piksel po izlaznom pikselu, i tretira RGB koji sedi ispod potpuno transparentnog piksela kao da će ga čitalac ikad videti. Prepravka u v3.100.0 zamenjuje tu jedinu putanju sa pet kernela, izmerenim pravilom izbora, i eksplicitnim budžetom radne memorije

Zašto smanjivanje čini skenirani tekst čupavim?

Jer tačkastano uzorkovanje odgovara na pogrešno pitanje. Kada se 300 DPI sken preusmeri na 150 DPI, svaki odredišni piksel stoji za blok od dva-puta-dva izvorna piksela, i najbliži sused zadržava jedan od četiri i odbacuje ostatak. Koji će preživeti zavisi od zaokruživanja, pa ivica poteza koja je bila glatko antialiased u izvoru postaje bacanje novčića po pikselu. Rezultat je klasična aliased lestvica duž ivica glifa, plus muar na polutonskim regionima gde su odbačeni uzorci slučajno nosili obrazac. Ovo je važnije u PDF-u nego na ekranu jer je šteta trajna. Image XObject nosi svoje podatke uzoraka uz /Width, /Height i /BitsPerComponent (ISO 32000-1 §8.9.5), i preuzorkovanje prepisuje sve tri unutar fajla. Loš zoom u pregledaču je kadar koji možete ponovo nacrtati, a PDFiumPas ima odvojenu mašineriju za to u render kešu i performansama zoom-a. Loš downsample je novi dokument koji predajete kupcu

Zašto smanjivanje najbližim susedom pokvari skenirani tekst u PDFiumPas-u za Delphi: svaki izlazni piksel zadržava jedan od četiri izvorna piksela i odbacuje ostatak, proizvodeći aliased ivice glifa i muar, koje pet kernela preuzorkovanja zamenjuju
Tačkastano uzorkovanje zadržava jedan izvorni piksel po izlaznom pikselu i baca ostala tri, po čemu PDFiumPas sada nudi pet kernela umesto jednog

Kako PDFiumPas meri detalj i bira kernel

PDFiumPas odlučuje po slici, ne po dokumentu. Pre izbora kernela izračunava normalizovan ocenjivač detalja luminanse iz omeđene mreže uzorkovanja: horizontalni i vertikalni koraci su (Width + 63) div 64 i (Height + 63) div 64, pa 12000-pikselni sken i 300-pikselna sličica koštaju otprilike isti prelaz 64-puta-64. Na svakom uzorkovanom mestu sabira apsolutnu razliku ka susedu desno i susedu ispod, kroz do tri kanala, pa deli brojem uzoraka puta 255. Ocena pada u 0 do 1, gde ravne poslovne grafike sede blizu nule i gusta fotografska tekstura se penje

Lestvica izbora zatim teče u fiksnom redosledu. Ako je ResampleFilter išta drugo nego pirfAdaptive, taj filter se koristi doslovno. Inače: 1-bitni sadržaj uzima pirfBilevel; ContentClass od piccLineArt uzima pirfBox; faktor razmere 4 ili više takođe uzima pirfBox, jer pri tom smanjenju prosek po površini je i najjeftiniji i najispravniji odgovor; piccPhoto, ocena detalja 0.08 ili viša, ili PreferredQuality 0.9 ili viši uzima pirfLanczos sa njegovim trolaptim kernelom; razmera 2 ili više ili kvalitet 0.7 ili viši uzima pirfBicubic radijusa 2; sve što ostane uzima pirfBilinear. Pošto TPdfImageOptimizeOptions.Default postavlja PreferredQuality na 0.85, podrazumevano pokretanje nikad ne pada nazad na bilinear osim ako je smanjenje blago a sadržaj ravan

Kako PDFiumPas bira kernel preuzorkovanja u Delphi-ju: omeđen prelaz šezdeset-puta-šezdeset proizvodi normalizovan ocenjivač detalja, pa fiksna lestvica uslova upućuje svaku sliku na bilevel, box, Lanczos, bicubic ili bilinear filter
Ocenjivač detalja košta isto na 12000-pikselnom skenu kao na sličici, a lestvica ispod njega staje na prvom uslovu koji se poklopi
uses
  PDFium;

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

Slika se dira tek kada veći od njenih horizontalnog i vertikalnog DPI plasmana, podeljen sa TargetDpi, dostigne MinDpiRatio. Taj guard postoji da 160 DPI fotografija usmerena na 150 DPI cilj ne bude ponovo kodovana za dobit od šest procenata koja košta jednu generaciju kvaliteta. Slike ispod MinDimension na bilo kojoj osi, 8 po podrazumevanju, se preskaču kao ikone ili linije

Zašto transparentni logo uhvati beli obrub?

Jer je boja ispod potpuno transparentnog piksela proizvoljna, i običan težinski prosek mu dozvoljava da glasa. Izvezite logo iz dizajn alata i nevidljiva margina je često bela, ili crna, ili šta god je platno bilo; alpha kanal je krije, i pravi zbir preko otiska kernela ga brzo vraća u vidljivu ivicu. PDFiumPas to izbegava nakupljanjem BGRA uzoraka u premultiplied obliku i poništavanjem premnoženja tek na odredišnom pikselu

Konkretno, svaki učesni uzorak dodaje kanal * alpha * težina akumulatoru boje, alpha * težina akumulatoru alfe, i težina zbiru težina. Odredišna boja se zatim deli akumulatorom alfe umesto zbirom težina, i to je korak koji biti: deljenje zbirom težina bi vuklo boju ka nevidljivim pikselima, dok deljenje nakupljenom alfom rekonstruiše boju na koju su vidljivi uzorci stvarno pristali. Odredišna alfa je odvojena veličina, 255 * AlphaSum / WeightSum. Ne-alpha formati dele zbirom težina kao obično, punjenje bajt FPDFBitmap_BGRx odredišta se upisuje kao konstanta 255, i svaki kanal je sabijen u 0 do 255 pre nego što se sačuva. Ta alfa obično potiče iz soft mask unosa u rečniku slike (ISO 32000-1 §11.4), koji je PDFium već sastavio u BGRA bafer koji resampler prima

Kako PDFiumPas uklanja beli oreol sa transparentnih PDF slika u Delphi-ju: uzorci se nakupljaju u premultiplied obliku, i odredišna boja se deli nakupljenom alfom umesto zbirom težina pa nevidljivi pikseli ne mogu da glasaju
Deljenje premnožene boje nakupljenom alfom rekonstruiše na čemu su se vidljivi uzorci složili, dok deljenje zbirom težina vuče ivicu ka nevidljivim pikselima
// Oblik unutrašnje nakupljajuće petlje, po učesnom izvornom uzorku
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;

// ... i na odredišnom pikselu, odmnoži naspram zbira alfe
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;

Držanje 1-bit line art-a van sive zone

Bilo koji kontinuirani kernel primenjen na bilevel sken proizvodi sivu, a siva je baš ono što fax-stil slika ne sme da sadrži. PDFiumPas zato ostavlja 1-bit slike na miru po podrazumevanju: PreserveBilevel je True u TPdfImageOptimizeOptions.Default, i takve slike sleću u SkippedCount nedirnute. Postavite ga na False i pirfBilevel putanja preuzima umesto kernela za izglađivanje. Ona šeta tačan izvorni pravougaonik koji pokriva svaki odredišni piksel, prosečava luminansu sa težinama 0.114, 0.587 i 0.299 u BGR redosledu memorije, i praguje rezultat na 127.5 u ravan 0 ili 255. Ništa srednje ne može biti upisano, pa ivice ostaju oštre i nijedan sivi oreol ne nastaje oko tankih poteza; alpha kanal BGRA izvora se prosečava normalno, i BGRx odredište dobija konstantu 255. Ako vam trebaju ležeći pikseli umesto manjeg dokumenta, izvlačenje slika iz PDF dokumenata je odvojena putanja

Šta se dešava kad slika premaši budžet radne memorije?

Ostavlja se baš kakva jeste, i broji se. MaxWorkingBytes podrazumeva 64 MiB i nameće se dvaput. Pre nego što se odredišna bitmapa stvori, PDFiumPas odbija sliku ako širina puta visina puta bajtova po pikselu premašuje budžet. Posle uspeha FPDFBitmap_CreateEx proverava ponovo koristeći pravi korak puta visinu, jer punjenje redova može gurati alokaciju preko granice koju je naivan proizvod prošao. Bilo koje odbijanje uništava odredište i ne vraća ništa. Budite jasni oko degradacije koju ovo implicira: slika preko budžeta se ne preuzorkuje nižim kvalitetom, i ne seče se na pločice. Original ostaje u dokumentu, BudgetExceededCount i SkippedCount oboje rastu, i pokretanje može zato izveštavati uspeh dok je dokument samo delimično optimizovan. To je namerno fail-safe ponašanje, ali znači da izveštaj nije opcionalna lektira. Postoji i odvojen režim greške: slike čiju bitmapu PDFium uopšte ne može proizvesti, poput CMYK, JPX, JBIG2 ili maskiranih izvora, povećavaju FailedCount umesto toga i isto tako ostaju nedirnute

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // koristi bilevel glas površine
  Options.ContentClass := piccPhoto;             // prisili Lanczos za foto skupove
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // rezerva za velike skenove
  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;

Čitanje izveštaja pre nego što pošaljete fajl

TPdfImageOptimizeReport je izgrađen da se dijagnostikuje, ne samo da se beleži. Uz OptimizedCount, SkippedCount i FailedCount izlaže po jedan brojač po kernelu, pa vam BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount i BilevelFilterCount kažu šta je adaptivno pravilo stvarno zaključilo o vašem korpusu. Sve-box rezultat znači da su smanjenja bila strma ili je sadržaj klasifikovan kao line art; sve-Lanczos rezultat na dokumentu za koji ste verovali da je line art je znak da ContentClass treba postaviti eksplicitno. AverageDetailScore je broj za poređenje sa pragom Lanczos od 0.08 pri štelovanju PreferredQuality, a PeakWorkingBytes pokazuje koliko je MaxWorkingBytes pokretanje stvarno trebalo. Neispravne opcije padaju glasno umesto tiho: nepozitivan TargetDpi, MinDpiRatio ispod 1, PreferredQuality van 0 do 1, ili nepozitivan MaxWorkingBytes dižu EPdfError pre nego što se bilo koja stranica dirne. I OptimizeImages uređuje samo dokument u memoriji; svaka izmenjena stranica se predaje sa FPDFPage_GenerateContent, posle čega sami pozivate SaveAs. Da oko-okomite šta se promenilo, renderujte dokumente pre i posle u bitmape kao što je opisano u pretvaranju PDF stranica u JPEG slike i uporedite ih na punom zoom-u

Adaptivno preuzorkovanje je jedna od onih funkcija koje su nevidljive kad rade i generišu tikete podrške kad ne rade, zato su merenje, alpha rukovanje i memorijski budžet morali sleteti zajedno umesto kao tri odvojena usavršavanja. Ako ovo procenjujete za Delphi, C++Builder ili Lazarus proizvod, potpuna API površina i detalji licenciranja su na stranici PDFiumPas Delphi PDFium komponente