Techninis straipsnis

Adaptuotas PDF vaizdų persamplavimas Delphi su PDFiumPas

Praėjus savaitei po glaudinimo funkcijos išleidimo atkeliauja du skundai: nuskaityta sutartis įgijo laiptuotas, plaukuotas raides, o viršelio puslapio skaidrus logotipas sėdi blyškios aureolės viduje. PDFiumPas į abu atsako vienoje vietoje. TPdf.OptimizeImages prieš mažindamas kiekvieną vaizdą jį išmatuoja, tada renkasi persamplavimo branduolį, o spalvas kaupia atsižvelgdamas į alfa kanalą

Taip ne visada buvo. Iki v3.100.0 tas pats metodas kiekvieną ne dvejetainį vaizdą mažino fiksuotu artimiausio kaimyno žingsniu — būtent tas algoritmas sukuria abu skundus: jis kiekvienam išvesties pikseliui paima po vieną šaltinio pikselį, o po visiškai permatomu pikseliu esantį RGB traktuoja taip, lyg skaitytojas jį kada nors pamatytų. Perrašymas v3.100.0 tą vienintelį kelią pakeičia penkiais branduoliais, išmatuota atrankos taisykle ir aiškiu darbo atminties biudžetu

Kodėl mažinant raišką nuskaitytas tekstas tampa laiptuotas?

Todėl, kad taškinis ėmimas atsako į netinkamą klausimą. Kai 300 DPI nuskaitymas perkeliamas į 150 DPI, kiekvienas paskirties pikselis atstovauja dviejų per du šaltinio pikselių blokui, o artimiausias kaimynas palieka vieną iš keturių ir atmeta likusius. Kuris išgyvena, sprendžia apvalinimas, tad šrate sklandžiai nugludintas brūkšnio kraštas kiekvienam pikseliui tampa monetos metimu. Rezultatas — klasikinis laiptuotas aliasingas palei glifų kraštus, o pusatonio rastruose dar ir muaras, ten, kur atmesti ėminiai atsitiktinai nešiojo raštą. PDF atveju tai svarbiau nei ekrane, nes žala yra nuolatinė. Vaizdo XObject savo ėminių duomenis neša kartu su /Width, /Height ir /BitsPerComponent (ISO 32000-1 §8.9.5), o persamplavimas perrašo visus tris pačiame faile. Blogas priartinimas žiūryklėje yra kadras, kurį galite perpiešti, ir PDFiumPas tam turi atskirą mechanizmą, aprašytą renderio talpyklos ir priartinimo našumo straipsnyje. Blogas sumažinimas yra naujas dokumentas, kurį įteikiate klientui

Kodėl artimiausio kaimyno mažinimas sugadina nuskaitytą tekstą PDFiumPas Delphi: kiekvienas išvesties pikselis palieka vieną iš keturių šaltinio pikselių ir atmeta kitus tris, sukurdamas aliased glifų kraštus ir muarą — ką pakeičia penki persamplavimo branduoliai
Taškinis ėmimas kiekvienam išvesties pikseliui palieka po vieną šaltinio pikselį, o kitus tris išmeta — todėl PDFiumPas dabar siūlo penkis branduolius vietoj vieno

Kaip PDFiumPas išmatuoja detales ir renkasi branduolį

PDFiumPas sprendžia kiekvienam vaizdui atskirai, o ne visam dokumentui. Prieš renkantis branduolį apskaičiuojamas normalizuotas šviesio detalių įvertis iš apriboto ėmimo tinklelio: horizontalus ir vertikalus žingsniai yra (Width + 63) div 64 ir (Height + 63) div 64, tad 12000 pikselių nuskaitymas ir 300 pikselių miniatiūra kainuoja maždaug tą patį 64 į 64 apėjimą. Kiekvienoje įmintoje vietoje sumuojamas absoliutus skirtumas iki kaimyno dešinėje ir kaimyno žemiau, per ne daugiau kaip tris kanalus, tada viskas dalijama iš ėminių skaičiaus ir 255 sandaugos. Įvertis patenka į intervalą nuo 0 iki 1, kur plokščia verslo grafika sėdi prie nulio, o tanki fotografinė tekstūra lipa aukštyn

Tada atrankos kopėčios žengia fiksuota tvarka. Jei ResampleFilter nustatytas į ką nors kitą nei pirfAdaptive, naudojamas tas filtras, kaip yra. Kitu atveju: 1 bito turinys gauna pirfBilevel; ContentClass, lygus piccLineArt, gauna pirfBox; mastelio koeficientas 4 ar didesnis taip pat gauna pirfBox, nes tokiame sumažinime ploto vidurkis yra ir pigiausias, ir teisingiausias atsakymas; piccPhoto, detalės įvertis 0.08 ar aukštesnis arba PreferredQuality 0.9 ar aukštesnė gauna pirfLanczos su jo trijų skilčių branduoliu; mastelis 2 ar didesnis arba kokybė 0.7 ar aukštesnė gauna pirfBicubic su spinduliu 2; viskas, kas liko, gauna pirfBilinear. Kadangi TPdfImageOptimizeOptions.Default nustato PreferredQuality į 0.85, numatytas paleidimas niekada negrįžta iki bilinear, nebent sumažinimas švelnus, o turinys plokščias

Kaip PDFiumPas pasirenka persamplavimo branduolį Delphi: apribotas 64 į 64 apėjimas duoda normalizuotą detalės įvertį, o fiksuotos sąlygų kopėčios nukreipia kiekvieną vaizdą į bilevel, box, Lanczos, bicubic arba bilinear filtrą
Detalės įvertis kainuoja tą patį 12000 pikselių nuskaityme ir miniatiūroje, o žemiau esančios kopėčios sustoja ties pirmąja atitinkančia sąlyga
uses
  PDFium;

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

Vaizdas liečiamas tik tada, kai didesnė iš jo horizontalios ir vertikalios išdėstymo DPI, padalinta iš TargetDpi, pasiekia MinDpiRatio. Ta apsauga yra tam, kad 160 DPI nuotrauka, taikoma į 150 DPI tikslą, nebūtų perkoduota dėl šešių procentų laimėjimo, kurio kaina — visas kokybės kartas. Vaizdai, mažesni už MinDimension bet kurioje ašyje — 8 pagal numatytuosius — praleidžiami kaip piktogramos ar linijos

Kodėl skaidrūs logotipai įgauna baltą pakraštį?

Nes spalva po visiškai permatomu pikseliu yra savavališka, o paprastas svertinis vidurkis leidžia jai balsuoti. Eksportuokite logotipą iš dizaino įrankio, ir nematoma paraštė dažnai būna balta, juoda arba tokia, kokia buvo drobė; alfa kanalas ją slepia, o tiesioginė suma per branduolio pėdsaką greitai ją vėl įmaišo į matomą kraštą. PDFiumPas to išvengia kaupdama BGRA ėminius iš anksto padauginta forma ir atšaukdama išankstinį dauginimą tik paskirties pikseliue

Konkrečiai: kiekvienas prisidedantis ėminis prideda channel * alpha * weight prie spalvos kaupiklio, alpha * weight prie alfos kaupiklio ir weight prie svorių sumos. Paskirties spalva tada dalijama iš alfos kaupiklio, o ne iš svorių sumos, ir būtent tas žingsnis lemia: dalijimas iš svorių sumos temptų spalvą link nematomų pikselių, o dalijimas iš sukauptos alfos atkuria spalvą, kuria matomi ėminiai iš tikrųjų sutarė. Paskirties alfa yra atskiras dydis, 255 * AlphaSum / WeightSum. Ne alfos formatai dalija iš svorių sumos kaip įprasta, FPDFBitmap_BGRx paskirties užpildymo baitas rašomas kaip pastovus 255, o kiekvienas kanalas prieš išsaugojimą suvaržomas į 0–255. Ta alfa paprastai kyla iš soft mask įrašo vaizdo žodyne (ISO 32000-1 §11.4) — PDFium jau yra sukomponavęs jį į BGRA buferį, kurį gauna persamplavimo kodas

Kaip PDFiumPas pašalina baltą aureolę iš skaidrių PDF vaizdų Delphi: ėminiai kaupiami iš anksto padauginta forma, o paskirties spalva dalijama iš sukauptos alfos vietoj svorių sumos, tad nematomi pikseliai negali balsuoti
Iš anksto padaugintos spalvos dalijimas iš sukauptos alfos atkuria tai, apie ką sutarė matomi ėminiai, o dalijimas iš svorių sumos tempia kraštą link nematomų pikselių
// Vidinio kaupimo ciklo pavidalas, kiekvienam prisidedančiam šaltinio ėminiui
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;

// ... o paskirties pikseliue atšaukiamas išankstinis dauginimas pagal alfos 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;

1 bitų linijinė grafika lieka už pilkos zonos ribų

Bet kuris tęstinis branduolys, pritaikytas dvejetainiam nuskaitymui, duoda pilką, o pilkos faksinio stiliaus vaizdas turėti negali. PDFiumPas todėl pagal numatytuosius 1 bito vaizdų neliečia: PreserveBilevel yra True TPdfImageOptimizeOptions.Default nustatymuose, ir tokie vaizdai patenka į SkippedCount nelieti. Nustatykite False, ir pirfBilevel kelias perima vietoj glotninamojo branduolio. Jis apėja tikslų šaltinio stačiakampį, dengiantį kiekvieną paskirties pikselį, vidutina šviesį su 0.114, 0.587 ir 0.299 svoriais BGR atminties tvarka ir slenksčiu 127.5 suverčia rezultatą į plokščią 0 arba 255. Nieko tarpinio įrašyti negalima, todėl kraštai lieka aštrūs ir aplink plonus brūkšnius nesusidaro pilka aureolė; BGRA šaltinio alfa kanalas vidutinamas įprastai, o BGRx paskirtis gauna pastovų 255. Jei jums reikia pačių pikselių duomenų, o ne mažesnio dokumento, atskiras kelias yra vaizdų išgavimas iš PDF dokumentų

Kas nutinka, kai vaizdas viršija darbo atminties biudžetą?

Jis paliekamas lygiai toks, koks buvo, ir suskaičiuojamas. MaxWorkingBytes pagal numatytuosius lygus 64 MiB ir vykdomas dukart. Prieš kuriant paskirties bitmap PDFiumPas atmeta vaizdą, jei plotio, aukščio ir baitų per pikselį sandauga viršija biudžetą. Kai FPDFBitmap_CreateEx pasiseka, tikrinama dar kartą su tikru stride ir aukščiu, nes eilučių užpildymas gali pervesti alokaciją už ribos, kurios naivioji sandauga neprieis. Bet koks atmetimas sunaikina paskirtį ir negrąžina nieko. Aiškiai suvokite šios degradacijos reikšmę: vaizdas, einantis per biudžetą, nėra persampluojamas žemesne kokybe ir nėra skaidomas į plyteles. Originalas lieka dokumente, BudgetExceededCount ir SkippedCount abu išauga, ir paleidimas gali pranešti sėkmę, nors dokumentas optimizuotas tik iš dalies. Tai sąmoninga fail-safe elgsena, bet vadinasi, ataskaita nėra nebūtinas skaitymas. Egzistuoja ir atskiras gedimo režimas: vaizdai, kurių bitmap PDFium iš viso negali pagaminti — CMYK, JPX, JBIG2 arba su kaukėmis — didina FailedCount ir taip pat paliekami nelieti

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // naudoti dvejetainio vaizdo ploto balsavimą
  Options.ContentClass := piccPhoto;             // priversti Lanczos nuotraukų rinkiniams
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // atsarga dideliems nuskaitymams
  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;

Ataskaitos skaitymas prieš išsiunčiant failą

TPdfImageOptimizeReport sukurtas diagnozuoti, o ne vien rašyti į žurnalą. Šalia OptimizedCount, SkippedCount ir FailedCount jis atskleidžia po skaitiklį kiekvienam branduoliui, tad BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount ir BilevelFilterCount pasako, ką adaptuotoji taisyklė iš tiesų nusprendė apie jūsų rinkinį. Vien box rezultatas reiškia, kad sumažinimai buvo statūs arba turinys klasifikuotas kaip linijinė grafika; vis Lanczos rezultatas dokumente, kurį laikėte linijine grafika, yra ženklas, kad ContentClass reikėtų nustatyti aiškiai. AverageDetailScore yra skaičius, kurį lyginti su 0.08 Lanczos riba derinant PreferredQuality, o PeakWorkingBytes rodo, kiek iš MaxWorkingBytes paleidimas iš tiesų prireikė. Netinkami parametrai žlunga garsiai, o ne tyliai: neigiamas arba nulinis TargetDpi, MinDpiRatio žemiau 1, PreferredQuality už 0–1 ribų arba neigiamas arba nulinis MaxWorkingBytes kelia EPdfError prieš paliečiant bent vieną puslapį. O OptimizeImages redaguoja tik dokumentą atmintyje; kiekvienas pakeistas puslapis įrašomas su FPDFPage_GenerateContent, o SaveAs po to vis tiek kviečiate patys. Kad akimis pamatytumėte, kas pasikeitė, atvaizduokite prieš ir po esančius dokumentus į bitmap, kaip aprašyta straipsnyje PDF puslapių konvertavimas į JPEG vaizdus, ir palyginkite pilnu priartinimu

Adaptuotas persamplavimas yra viena iš tų funkcijų, kurios nematomos, kai veikia, ir kurios kuria palaikymo bilietus, kai neveikia — todėl matavimas, alfa apdorojimas ir atminties biudžetas turėjo atkeliauti kartu, o ne kaip trys atskiri patobulinimai. Jei vertinate tai Delphi, C++Builder arba Lazarus produktui, pilnas API paviršius ir licencijavimo detalės yra PDFiumPas Delphi PDFium komponento puslapyje