Tehnički članak

Adaptivni resampling PDF slika u Delphiju s PDFiumPasom

Dvije pritužbe stignu tjedan nakon što kompresijska značajka krene u prodaju: skenirani ugovor sada ima stubasto, dlakavo oblikovana slova, a transparentni logotip na naslovnici sjedi unutar blijedog oreola. PDFiumPas odgovara na oboje na jednom mjestu. TPdf.OptimizeImages izmjeri svaku sliku prije nego što je smanji, a zatim bira jezgru resamplinga i akumulira boju u obliku osviještenom na alfu

To nije uvijek bilo tako. Prije v3.100.0 ista je metoda smanjivala svaku nebilevelnu sliku fiksiranim korakom najbližeg susjeda, što je točno algoritam koji proizvodi obje pritužbe: točkastim uzorkovanjem uzima jedan izvorni piksel po izlaznom pikselu, a RGB ispod potpuno transparentnog piksela tretira kao da bi ga čitatelj ikad vidio. Prepravka u v3.100.0 taj jedini put zamjenjuje s pet jezgri, izmjerenim pravilom odabira i izričitim proračunom radne memorije

Zašto smanjivanje čini skenirani tekst nazobčenim?

Jer točkast uzorkovanje odgovara na pogrešno pitanje. Kad se 300 DPI sken preusmjeri na 150 DPI, svaki odredišni piksel predstavlja blok izvornih piksela dva puta dva, a najbliži susjed zadrži jedan od četiri i odbaci ostatak. Preživjeli ovisi o zaokruživanju, pa rub poteza koji je u izvoru bio glatko zaglađen postaje bacanje novčića po pikselu. Rezultat je klasična nazubljena stepenica uz rubove znakova, plus moare na rasterskim područjima gdje su odbačeni uzorci slučajno nosili obrazac. U PDF-u je to važnije nego na ekranu jer je šteta trajna. Slika XObject nosi svoje podatke uzoraka uz /Width, /Height i /BitsPerComponent (ISO 32000-1 §8.9.5), a resampling prepisuje sva tri unutar datoteke. Loš zoom u pregledniku je kadar koji možete ponovno iscrtati, a PDFiumPas za to ima zasebnu mehaniku u render kešu i performansama zooma. Loš downsample je novi dokument koji predajete kupcu

Zašto smanjivanje najbližim susjedom uništava skenirani tekst u PDFiumPasu za Delphi: svaki izlazni piksel zadrži jedan od četiri izvorna piksela i odbaci ostatak, stvarajući nazubljene rubove znakova i moare, što pet jezgre resamplinga zamjenjuju
Točkast uzorkovanje zadrži jedan izvorni piksel po izlaznom pikselu i baci ostala tri, zato PDFiumPas sada nudi pet jezgri umjesto jedne

Kako PDFiumPas mjeri detalje i bira jezgru

PDFiumPas odlučuje po slici, ne po dokumentu. Prije odabira jezgre računa normalizirani rezultat detalja luminancije iz ograničene mreže uzorkovanja: vodoravni i okomiti koraci su (Width + 63) div 64 i (Height + 63) div 64, pa 12000-pikselni sken i 300-pikselna minijatura koštaju otprilike isti predlaz 64 puta 64. Na svakom uzorkovanom položaju zbraja apsolutnu razliku prema susjedu desno i susjedu ispod, kroz najviše tri kanala, a zatim dijeli brojem uzoraka puta 255. Rezultat pada u raspon 0 do 1, gdje ravna poslovna grafika sjedi blizu nule, a gusta fotografska tekstura raste

Ljestvica odabira zatim prolazi u fiksnom redoslijedu. Ako je ResampleFilter išta drugo nego pirfAdaptive, taj se filter koristi doslovno. Inače: 1-bitni sadržaj uzima pirfBilevel; ContentClass od piccLineArt uzima pirfBox; faktor mjerila 4 ili više također uzima pirfBox, jer je pri tom smanjenju prosjek površine i najjeftiniji i najtočniji odgovor; piccPhoto, rezultat detalja 0.08 ili viši ili PreferredQuality 0.9 ili viši uzimaju pirfLanczos s njegovom trokrilom jezgrom; mjerilo 2 ili više ili kvaliteta 0.7 ili viša uzima pirfBicubic radijusa 2; sve što ostane uzima pirfBilinear. Budući da TPdfImageOptimizeOptions.Default postavlja PreferredQuality na 0.85, zadano pokretanje nikad ne pada na bilinearno osim ako je smanjenje blago a sadržaj ravan

Kako PDFiumPas u Delphiju bira jezgru resamplinga: ograničeni predlaz 64 puta 64 daje normalizirani rezultat detalja, a zatim fiksna ljestvica uvjeta svaku sliku usmjeri na bilevel, box, Lanczos, bicubic ili bilinear filter
Rezultat detalja košta isto na 12000-pikselnom skenu i na minijaturi, a ljestvica ispod njega staje na prvom uvjetu koji odgovara
uses
  PDFium;

procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InputFile;
    // Defaults: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
    // MinDimension 8, pirfAdaptive, piccAuto, quality 0.85, 64 MiB budget.
    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 samo kad veća od njezina vodoravnog i okomitog DPI smještaja, podijeljena s TargetDpi, dosegne MinDpiRatio. Taj osigurač postoji da 160 DPI fotografija namijenjena cilju od 150 DPI ne bude ponovno kodirana za dobit od šest posto koja košta jednu generaciju kvalitete. Slike ispod MinDimension po bilo kojoj osi, 8 po zadanom, preskaču se kao ikone ili crtice

Zašto transparentni logotipi dobivaju bijeli obrub?

Jer je boja ispod potpuno transparentnog piksela proizvoljna, a obična težinska sredina dopusti joj da glasa. Izvezite logotip iz dizajnerskog alata i nevidljiva margina je često bijela, ili crna, ili bilo što što je platno bilo; alfa kanal je sakriva, a ravna suma preko otiska jezgre odmah je natapa u vidljivi rub. PDFiumPas to izbjegava tako da BGRA uzorke akumulira u premnoženoj formi, a premnoženje poništava tek na odredišnom pikselu

Konkretno, svaki doprinoseći uzorak dodaje channel * alpha * weight akumulatoru boje, alpha * weight alfa akumulatoru i weight sumi težina. Odredišna se boja zatim dijeli alfa akumulatorom, a ne sumom težina, i to je korak koji glavu vuče: dijeljenje sumom težina vuklo bi boju prema nevidljivim pikselima, dok dijeljenje akumuliranom alfom rekonstruira boju na kojoj se vidljivi uzorci stvarno složili. Odredišna alfa je zasebna veličina, 255 * AlphaSum / WeightSum. Formati bez alfe dijele sumom težina kao i obično, bajt popune FPDFBitmap_BGRx odredišta upisuje se kao konstantna 255, a svaki se kanal steže u raspon 0 do 255 prije spremanja. Ta alfa obično potječe iz unosa meke maske u rječniku slike (ISO 32000-1 §11.4), koju je PDFium već kompozitirao u BGRA međuspremnik koji resampler prima

Kako PDFiumPas u Delphiju uklanja bijeli oreol s transparentnih PDF slika: uzorci se akumuliraju u premnoženoj formi, a odredišna se boja dijeli akumuliranom alfom umjesto sumom težina tako da nevidljivi pikseli ne mogu glasati
Dijeljenje premnožene boje akumuliranom alfom rekonstruira ono na čemu su se vidljivi uzorci složili, dok dijeljenje sumom težina vuče rub prema nevidljivim pikselima
// Shape of the inner accumulation loop, per contributing source sample
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;

// ... and at the destination pixel, unpremultiply against the alpha sum
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-bitne linijske grafike izvan sive zone

Bilo koja kontinuirana jezgra primijenjena na bilevelni sken proizvodi sivu, a siva je upravo ono što fax stilu slike nije dopušteno sadržavati. PDFiumPas zato po zadanom ostavlja 1-bitne slike na miru: PreserveBilevel je True u TPdfImageOptimizeOptions.Default, i takve slike završavaju u SkippedCount nedirnute. Postavite ga na False i put pirfBilevel preuzme umjesto jezgre zaglađivanja. Ona obilazi točan izvorni pravokut koji pokriva svaki odredišni piksel, prosječi luminanciju s težinama 0.114, 0.587 i 0.299 u BGR redoslijedu memorije, a rezultat svodi pragom 127.5 na ravnu 0 ili 255. Ništa posredno ne može se upisati, pa rubovi ostaju oštri i oko tankih poteza ne nastaje sivi oreol; alfa kanal BGRA izvora prosječi se normalno, a BGRx odredište dobiva konstantnu 255. Ako trebate piksele u pozadini, a ne manji dokument, izdvajanje slika iz PDF dokumenata je zaseban put

Što se događa kad slika premaši proračun radne memorije?

Ostavlja se točno kakva je bila, i ubraja se. MaxWorkingBytes je po zadanom 64 MiB i provodi se dvaput. Prije stvaranja odredišne bitmape PDFiumPas odbije sliku ako širina puta visina puta bajtovi po pikselu premašuju proračun. Nakon što uspije FPDFBitmap_CreateEx provjerava opet koristeći stvarni korak reda puta visinu, jer ispunjavanje redova može gurnuti alokaciju preko granice koju je naivni umnožak očistio. Bilo koje odbijanje uništi odredište i ne vraća ništa. Budite jasni oko degradacije koju to povlači: slika preko proračuna ne resamplinga se nižom kvalitetom, i ne dijeli se na pločice. Izvornik ostaje u dokumentu, BudgetExceededCount i SkippedCount oboje rastu, pa pokretanje može izvještati uspjeh dok je dokument samo djelomično optimiran. To je namjerno fail-safe ponašanje, ali znači da izvještaj nije obavezno štivo. Postoji i zaseban oblik neuspjeha: slike čiju bitmapu PDFium uopće ne može proizvesti, poput CMYK, JPX, JBIG2 ili maskiranih izvora, umjesto toga povećavaju FailedCount i jednako 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;              // use the bilevel area vote
  Options.ContentClass := piccPhoto;             // force Lanczos for photo sets
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // headroom for large scans
  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 izvještaja prije otpreme datoteke

TPdfImageOptimizeReport je građen za dijagnozu, ne samo za zapisivanje. Uz OptimizedCount, SkippedCount i FailedCount izlaže jedan brojač po jezgri, pa vam BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount i BilevelFilterCount kažu što je prilagodljivo pravilo stvarno zaključilo o vašem korpusu. Rezultat samo box znači da su smanjenja bila strma ili da je sadržaj razvrstan kao linijska grafika; rezultat samo Lanczos na dokumentu za koji ste vjerovali da je linijska grafika znak je da treba izričito postaviti ContentClass. AverageDetailScore je broj koji uspoređujete s pragom 0.08 za Lanczos kad štimate PreferredQuality, a PeakWorkingBytes pokazuje koliko je MaxWorkingBytes pokretanje stvarno trebalo. Neispravne opcije propadaju glasno, ne tiho: nepozitivan TargetDpi, MinDpiRatio ispod 1, PreferredQuality izvan 0 do 1 ili nepozitivan MaxWorkingBytes podiže EPdfError prije nego bilo koja stranica bude dirnuta. A OptimizeImages uređuje samo dokument u memoriji; svaka izmijenjena stranica predaje se s FPDFPage_GenerateContent, nakon čega i dalje sami zovete SaveAs. Da pogledom usporedite što se promijenilo, iscrtajte dokumente prije i poslije u bitmape kao što opisuje pretvaranje PDF stranica u JPEG slike i usporedite ih na punom zoomu

Prilagodljivi resampling je jedna od onih značajki koja je nevidljiva kad radi i stvara zahtjeve za podršku kad ne radi, zato su mjerenje, rukovanje alfom i proračun memorije morale stići zajedno, a ne kao tri zasebna usavršenja. Ako ovo procjenjujete za proizvod u Delphi, C++Builder ili Lazarusu, potpuna API površina i detalji licenciranja su na stranici PDFiumPas Delphi PDFium komponente