Tekninen artikkeli

PDF-kuvien uudelleenotanta Delphissä PDFiumPasilla

Viikko pakkaustoiminnon julkaisun jälkeen saapuu kaksi valitusta: skannatun sopimuksen kirjainmuodot ovat portaittaisia ja karheita, ja kansilehden läpinäkyvä logo makaa vaalean halon sisällä. PDFiumPas vastaa molempiin yhdessä paikassa. TPdf.OptimizeImages mittaa jokaisen kuvan ennen kuin pienentää sen, ja valitsee sitten uudelleenotantakernelin ja akumuloi värit alfaa huomioivassa muodossa

Näin ei aina ollut. Ennen versiota v3.100.0 sama metodi pienensi jokaisen ei-kaksitasoisen kuvan kiinteällä nearest neighbour -vaiheella, joka on täsmälleen se algoritmi, joka tuottaa molemmat valitukset: se ottaa pistenäytteen yhdestä lähdepikselistä jokaista kohdepikseliä kohden ja kohtelee täysin läpinäkyvän pikselin alla olevaa RGB-arvoa ikään kuin lukija näkisi sen koskaan. v3.100.0:n uudelleenkirjoitus korvaa sen yhden polun viidellä kernelillä, mitatulla valintasäännöllä ja eksplisiittisellä työmuistibudjetilla

Miksi pienentäminen tekee skannatusta tekstistä rosoista?

Koska pistenäytteistys vastaa väärään kysymykseen. Kun 300 DPI:n skannaus kohdistetaan 150 DPI:hen, jokainen kohdepikseli edustaa kahden kertaa kahden lähdepikselin lohkoa, ja nearest neighbour säilyttää yhden neljästä ja hylkää loput. Mikä niistä jää henkiin riippuu pyöristyksestä, joten viivan reuna, joka oli lähteessä tasaisen antialiasoitu, muuttuu pikselikohtaiseksi kolikonheitoksi. Tuloksena on klassinen portaittainen aliasing glyyfien reunoilla sekä moiré rasterialueilla, joissa hylätyt näytteet sattuivat kantamaan kuviota. Tämä merkitsee PDF:ssä enemmän kuin näytöllä, koska vahinko on pysyvä. Kuva-XObject kantaa näytetietonsa /Width-, /Height- ja /BitsPerComponent-arvojen ohella (ISO 32000-1 §8.9.5), ja uudelleenotanta kirjoittaa kaikki kolme tiedoston sisällä. Huono zoomaus katselimessa on kehys, jonka voi piirtää uudelleen, ja PDFiumPasilla on siihen erillinen koneisto artikkelissa renderöintivälimuisti ja zoomauksen suorituskyky. Huono pienentäminen on uusi asiakirja, jonka toimitat asiakkaalle

Miksi nearest neighbour -pienentäminen pilaa skannatun tekstin PDFiumPasissa Delphissä: jokainen kohdepikseli säilyttää yhden neljästä lähdepikselistä ja hylkää loput, mikä tuottaa portaittaisia glyyfireunoja ja moirét, jotka viisi uudelleenotantakerneliä korvaa
Pistenäytteistys säilyttää yhden lähdepikselin jokaista kohdepikseliä kohden ja heittää muut kolme pois, minkä vuoksi PDFiumPas tarjoaa nyt viisi kerneliä yhden sijaan

Miten PDFiumPas mittaa yksityiskohtaisuuden ja valitsee kernelin

PDFiumPas päättää kuvittain, ei asiakirjittain. Ennen kernelin valintaa se laskee normalisoidut luminanssin yksityiskohtapisteet rajatulta näytteistysruudukolta: vaakasuora ja pystysuora askel ovat (Width + 63) div 64 ja (Height + 63) div 64, joten 12000 pikselin skannaus ja 300 pikselin pikkukuva maksavat molemmat suunnilleen saman 64 kertaa 64 -kierroksen. Jokaisessa näytteistetyssä kohdassa se summaa itseisarvoeron oikeanpuoleiseen naapuriin ja alla olevaan naapuriin korkeintaan kolmen kanavan yli ja jakaa sitten tuloksen näytemäärän kertaa 255 tulolla. Pisteet asettuvat välille 0–1, jossa tasaiset liiketoimintagrafiikat pysyttelevät nollan lähellä ja tiheä valokuvamainen tekstuuri kiipeää ylöspäin

Valintatikapuu kulkee sitten kiinteässä järjestyksessä. Jos ResampleFilter on mikä tahansa muu kuin pirfAdaptive, kyseistä suodatinta käytetään sellaisenaan. Muussa tapauksessa: 1-bittinen sisältö saa suodattimen pirfBilevel; ContentClass-arvo piccLineArt saa suodattimen pirfBox; skaalauskerroin 4 tai suurempi saa myös suodattimen pirfBox, koska kyseisessä pienennyksessä aluekeskiarvo on sekä halvin että oikein vastaus; piccPhoto, yksityiskohtapisteet 0.08 tai enemmän tai PreferredQuality 0.9 tai enemmän saa suodattimen pirfLanczos kolmihuippuisella kernelillään; skaala 2 tai enemmän tai laatu 0.7 tai enemmän saa suodattimen pirfBicubic säteellä 2; kaikki jäljelle jäävä saa suodattimen pirfBilinear. Koska TPdfImageOptimizeOptions.Default asettaa arvon PreferredQuality 0.85, oletusajo ei koskaan palaudu bilineariin, ellei pienennys ole lievä ja sisältö tasaista

Miten PDFiumPas valitsee uudelleenotantakernelin Delphissä: rajattu 64 kertaa 64 -kierros tuottaa normalisoidut yksityiskohtapisteet, minkä jälkeen kiinteä ehtojen tikapuu ohjaa jokaisen kuvan bilevel-, box-, Lanczos-, bicubic- tai bilinear-suodattimeen
Yksityiskohtapisteet maksavat saman 12000 pikselin skannauksessa kuin pikkukuvassa, ja sen alapuolinen tikapuu pysähtyy ensimmäiseen täsmäävään ehtoon
uses
  PDFium;

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

Kuvaan kosketaan vain, kun sen vaakasuoran ja pystysuoran sijoitus-DPI:n suurempi, jaettuna TargetDpi-arvolla, saavuttaa arvon MinDpiRatio. Se suojaus on olemassa, jotta 160 DPI:n valokuvaa, joka tähtää 150 DPI:n kohteeseen, ei uudelleenkoodata kuuden prosentin hyödyn takia, joka maksaisi yhden laatusukupolven. Kuvat, jotka ovat kummallakin akselilla alle arvon MinDimension (oletuksena 8), ohitetaan kuvakkeina tai viivoina

Miksi läpinäkyvät logot saavat valkoisen halon?

Koska täysin läpinäkyvän pikselin alla oleva väri on mielivaltainen, ja tavallinen painotettu keskiarvo antaa sen äänestää. Vie logo suunnittelutyökalusta ja näkymätön reunus on usein valkoinen, tai musta, tai mitä kangas sattui olemaan; alfakanava piilottaa sen, ja suora summa kernelin jalanjäljen yli sekoittaa sen nopeasti takaisin näkyvään reunaan. PDFiumPas välttää tämän akumuloimalla BGRA-näytteet esikerroittuneessa muodossa ja kumoamalla esikerroittumisen vasta kohdepikselissä

Konkreettisesti jokainen osallistuva näyte lisää channel * alpha * weight väriakumulaattoriin, alpha * weight alfa-akumulaattoriin ja weight painosummaan. Kohdeväri jaetaan sitten alfa-akumulaattorilla eikä painosummalla, ja se on askel, jolla on merkitystä: jakaminen painosummalla vetäisi värin kohti näkymättömiä pikseleitä, kun taas jakaminen akumuloidulla alfalla rakentaa uudelleen värin, josta näkyvät näytteet oikeasti olivat yhtä mieltä. Kohdealfa on erillinen suure, 255 * AlphaSum / WeightSum. Ei-alfa-formaatit jakavat painosummalla kuten yleensä, FPDFBitmap_BGRx-kohteen täyttötavu kirjoitetaan vakioksi 255, ja jokainen kanava puristetaan väliin 0–255 ennen tallennusta. Kyseinen alfa alkaa yleensä kuvasanakirjan soft mask -merkinnästä (ISO 32000-1 §11.4), jonka PDFium on jo yhdistänyt siihen BGRA-puskuriin, jonka uudelleenotostin vastaanottaa

Miten PDFiumPas poistaa valkoisen halon läpinäkyvistä PDF-kuvista Delphissä: näytteet akumuloidaan esikerroittuneessa muodossa, ja kohdeväri jaetaan akumuloidulla alfalla painosumman sijaan, jotta näkymättömät pikselit eivät voi äänestää
Esikerroittuneen värin jakaminen akumuloidulla alfalla rakentaa sen, mistä näkyvät näytteet olivat yhtä mieltä, kun taas jakaminen painosummalla vetää reunan kohti näkymättömiä pikseleitä
// Sisimmän akumulointisilmukan muoto, jokaista osallistuvaa lähdenäytettä kohden
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;

// ... ja kohdepikselissä esikerroittuminen kumotaan alfsummaa vasten
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-bittinen viivapiirros pysyy harmausalueen ulkopuolella

Mikä tahansa jatkuva kerneli kaksitasoiseen skannaukseen sovellettuna tuottaa harmaata, ja harmaa on täsmälleen se, mitä faksityylinen kuva ei saa sisältää. PDFiumPas jättää siksi 1-bittiset kuvat rauhaan oletuksena: PreserveBilevel on True asetuksessa TPdfImageOptimizeOptions.Default, ja tällaiset kuvat päätyvät koskemattomina laskuriin SkippedCount. Aseta arvoksi False, niin pirfBilevel-polku ottaa ohjat tasoittavan kernelin sijaan. Se kulkee sen tarkan lähdesuorakulmion läpi, joka kattaa jokaisen kohdepikselin, keskiarvoistaa luminanssin painoilla 0.114, 0.587 ja 0.299 BGR-muistijärjestyksessä ja kynnystää tuloksen arvossa 127.5 puhtaaksi 0:ksi tai 255:ksi. Mitään välisävyä ei voi kirjoittaa, joten reunat pysyvät terävinä eikä harmaata haloa muodostu ohuiden viivojen ympärille; BGRA-lähteen alfakanava keskiarvoistetaan tavallisesti, ja BGRx-kohde saa vakion 255. Jos tarvitset taustalla olevat pikselit eikä pienempää asiakirjaa, kuvien poimiminen PDF-asiakirjoista on erillinen polku

Mitä tapahtuu, kun kuva ylittää työmuistibudjetin?

Se jätetään täsmälleen sellaiseksi kuin se oli, ja se lasketaan. MaxWorkingBytes on oletuksena 64 MiB ja sitä valvotaan kahdesti. Ennen kohdebittikartan luomista PDFiumPas hylkää kuvan, jos leveys kertaa korkeus kertaa tavua pikseliä kohden ylittää budjetin. Sen jälkeen kun FPDFBitmap_CreateEx on onnistunut, se tarkistaa uudelleen käyttäen oikeaa strideä kertaa korkeus, koska rivitäyttö voi työntää allokoinnin ohi rajan, jonka naiivi tulo päästi läpi. Kumpikin hylkäys tuhoaa kohteen eikä palauta mitään. Ole selkeä siitä heikkenemisestä, mitä tämä merkitsee: budjetin ylittänyttä kuvaa ei uudelleenotosteta alhaisemmalla laadulla, eikä sitä jaeta laatoiksi. Alkuperäinen jää asiakirjaan, BudgetExceededCount ja SkippedCount kasvavat molemmat, ja ajo voi siksi ilmoittaa onnistumisen, vaikka asiakirja on optimoitu vain osittain. Kyseessä on tahallinen fail-safe-käyttäytyminen, mutta se tarkoittaa, ettei raportti ole valinnaista luettavaa. Myös erillinen vikatila on olemassa: kuvat, joiden bittikarttaa PDFium ei pysty tuottamaan lainkaan, kuten CMYK-, JPX-, JBIG2- tai maskatut lähteet, kasvattavat laskuria FailedCount ja jätetään vastaavasti koskemattomiksi

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // käytä bilevel-alueen äänestystä
  Options.ContentClass := piccPhoto;             // pakota Lanczos valokuvajoukoille
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // varatilaa suurille skannauksille
  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;

Raportin lukeminen ennen tiedoston toimittamista

TPdfImageOptimizeReport on rakennettu diagnosoitavaksi, ei pelkästään lokitettavaksi. Arvojen OptimizedCount, SkippedCount ja FailedCount ohella se näyttää yhden laskurin per kerneli, joten BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount ja BilevelFilterCount kertovat, mihin adaptiivinen sääntö aineistosi suhteen oikeasti päätyi. Kaikki-box-tulos tarkoittaa, että pienennykset olivat jyrkkiä tai sisältö luokiteltiin viivapiirrokseksi; kaikki-Lanczos-tulos asiakirjassa, jonka uskoit olevan viivapiirros, on merkki siitä, että ContentClass kannattaa asettaa eksplisiittisesti. AverageDetailScore on luku, jota verrataan Lanczos-kynnykseen 0.08 säädetessä asetusta PreferredQuality, ja PeakWorkingBytes näyttää, kuinka suuren osan arvosta MaxWorkingBytes ajo oikeasti tarvitsi. Virheelliset asetukset epäonnistuvat äänekkäästi eivätkä hiljaa: ei-positiivinen TargetDpi, MinDpiRatio alle 1, PreferredQuality välin 0–1 ulkopuolella tai ei-positiivinen MaxWorkingBytes nostaa poikkeuksen EPdfError ennen kuin yhtään sivua on koskettu. Ja OptimizeImages muokkaa vain muistissa olevaa asiakirjaa; jokainen muokattu sivu vahvistetaan funktiolla FPDFPage_GenerateContent, minkä jälkeen kutsut silti itse funktiota SaveAs. Nähdäksesi silmämääräisesti, mitä muuttui, renderöi alkuperäisen ja lopullisen asiakirjan bittikartoiksi artikkelissa PDF-sivujen muuntaminen JPEG-kuviksi kuvatulla tavalla ja vertaile niitä täysillä zoomauksilla

Adaptiivinen uudelleenotanta on yksi niistä ominaisuuksista, jotka ovat näkymättömiä toimiessaan ja tuottavat tukipyyntöjä toimiessaan väärin, minkä vuoksi mittaus, alfan käsittely ja muistibudjetti piti saada valmiiksi yhdessä eikä kolmena erillisenä viimeistelynä. Jos arvioit tätä Delphi-, C++Builder- tai Lazarus-tuotteeseen, koko API-pinta ja lisensointitiedot ovat PDFiumPas Delphi PDFium -komponentin sivulla