Teknisk artikel

Adaptiv PDF-billedresampling i Delphi med PDFiumPas

To klager ankommer ugen efter, at en komprimeringsfunktion udrulles: den scannede kontrakt har nu trappeformede, lodne bogstavformer, og det transparente logo på forsiden sidder inde i en bleg halo. PDFiumPas besvarer begge ét sted. TPdf.OptimizeImages måler hvert billede, før det formindsker det, vælger derefter en resamplingskerne og akkumulerer farve i alpha-bevidst form

Det var ikke altid sådan. Før v3.100.0 nedskalerede samme metode alle ikke-bilevel-billeder med et fast nærmeste-nabo-trin, hvilket er præcis den algoritme, der frembringer begge klager: den punktsampler én kildepixel pr. outputpixel, og den behandler RGB under en fuldt transparent pixel, som om en læser nogensinde skulle se den. Omskrivningen i v3.100.0 erstatter den eneste sti med fem kerner, en målt udvælgelsesregel og et eksplicit arbejdshukommelsesbudget

Hvorfor får nedskalering scannet tekst til at se takket ud?

Fordi punktsampling besvarer det forkerte spørgsmål. Når en 300 DPI-scan rettes mod 150 DPI, repræsenterer hver destinationspixel en to-gange-to-blok af kildepixels, og nærmeste nabo beholder én af de fire og kasserer resten. Hvilken én, der overlever, afhænger af afrunding, så en stregkant, der var glat antialiased i kilden, bliver et møntkast pr. pixel. Resultatet er den klassiske aliaserede trappeform langs glyfkanter plus moiré på rasterområder, hvor de kasserede prøver tilfældigt bar mønsteret. Det betyder mere i en PDF end på skærmen, fordi skaden er permanent. Et image XObject bærer sine prøvedata sammen med /Width, /Height og /BitsPerComponent (ISO 32000-1 §8.9.5), og resampling omskriver alle tre inde i filen. Et dårligt zoom i en fremviser er en ramme, du kan tegne om, og PDFiumPas har separat maskineri til det i render cache og zoom-ydelse. En dårlig nedskalering er et nyt dokument, du rækker kunden

Hvorfor nearest-neighbour-nedskalering ødelægger scannet tekst i PDFiumPas til Delphi: hver outputpixel beholder én af fire kildepixels og kasserer resten, hvilket giver aliaserede glyfkanter og moiré, som de fem resamplingskerner erstatter
Punktsampling beholder én kildepixel pr. outputpixel og smider de andre tre væk, og det er derfor, PDFiumPas nu tilbyder fem kerner i stedet for én

Hvordan PDFiumPas måler detaljer og vælger en kerne

PDFiumPas beslutter pr. billede, ikke pr. dokument. Før den vælger en kerne, beregner den en normaliseret luminansdetaljescore fra et afgrænset samplinggitter: de vandrette og lodrette trin er (Width + 63) div 64 og (Height + 63) div 64, så en 12000-pixel-scan og en 300-pixel-thumbnail begge koster omkring samme 64-gange-64-svab. Ved hver sampled position summerer den den absolutte forskel til naboen mod højre og naboen nedenunder, på op til tre kanaler, og dividerer derefter med prøveantallet gange 255. Scoren lander mellem 0 og 1, hvor flad forretningsgrafik ligger nær nul, og tæt fotografisk tekstur klatrer

Udvælgelsesstigen kører derefter i fast rækkefølge. Hvis ResampleFilter er noget andet end pirfAdaptive, bruges det filter ordret. Ellers: 1-bit-indhold får pirfBilevel; en ContentClass af piccLineArt får pirfBox; en skaleringsfaktor på 4 eller mere får også pirfBox, for ved den formindskelse er et arealgennemsnit både det billigste og det mest korrekte svar; piccPhoto, en detaljescore på 0,08 eller højere eller en PreferredQuality på 0,9 eller højere får pirfLanczos med sin tre-lobe-kerne; en skala på 2 eller mere eller en kvalitet på 0,7 eller højere får pirfBicubic med radius 2; alt tilbage får pirfBilinear. Da TPdfImageOptimizeOptions.Default sætter PreferredQuality til 0,85, falder en standardkørsel aldrig tilbage til bilinear, medmindre formindskelsen er mild, og indholdet er fladt

Hvordan PDFiumPas vælger en resamplingskerne i Delphi: et afgrænset 64-gange-64-svab giver en normaliseret detaljescore, hvorefter en fast stige af betingelser ruter hvert billede til bilevel-, box-, Lanczos-, bicubic- eller bilinearfilteret
Detaljescoren koster det samme på en 12000-pixel-scan som på en thumbnail, og stigen under den stopper ved den første betingelse, der matcher
uses
  PDFium;

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

Et billede berøres kun, når den største af dets vandrette og lodrette placerings-DPI divideret med TargetDpi når MinDpiRatio. Den vagt findes, så et 160 DPI-foto rettet mod et 150 DPI-mål ikke re-kodes for en gevinst på seks procent, der koster en generation af kvalitet. Billeder under MinDimension på en af akserne, 8 som standard, springes over som ikoner eller linjer

Hvorfor får transparente logoer en hvid kantglød?

Fordi farven under en fuldt transparent pixel er vilkårlig, og et simpelt vægtet gennemsnit lader den stemme. Eksportér et logo fra et designværktøj, og den usynlige margen er ofte hvid, eller sort, eller hvad lærredet nu var; alfakanalen skjuler den, og en lige sum over kerneaftrykket blander den straks tilbage i den synlige kant. PDFiumPas undgår det ved at akkumulere BGRA-prøver i præmultiplieret form og kun fortryde præmultiplikationen ved destinationspixel

Konkret tilføjer hver bidragende prøve channel * alpha * weight til farveakkumulatoren, alpha * weight til en alpha-akkumulator og weight til vægtsummen. Destinationsfarven divideres derefter med alpha-akkumulatoren frem for med vægtsummen, og det er trinnet, der tæller: at dividere med vægtsummen ville trække farven mod de usynlige pixels, mens at dividere med den akkumulerede alpha rekonstruerer farven, som de synlige prøver reelt var enige om. Destinationens alpha er en separat størrelse, 255 * AlphaSum / WeightSum. Ikke-alpha-formater dividerer med vægtsummen som sædvanlig, paddingbyten af en FPDFBitmap_BGRx-destination skrives som konstanten 255, og hver kanal klemmes til mellem 0 og 255, før den gemmes. Den alpha stammer normalt fra en soft mask-post i billedordbogen (ISO 32000-1 §11.4), som PDFium allerede har kompositeret ind i den BGRA-buffer, resampleren modtager

Hvordan PDFiumPas fjerner den hvide halo fra transparente PDF-billeder i Delphi: prøver akkumuleres i præmultiplieret form, og destinationsfarven divideres med den akkumulerede alpha i stedet for vægtsummen, så usynlige pixels ikke kan stemme
At dividere den præmultiplierede farve med den akkumulerede alpha rekonstruerer, hvad de synlige prøver var enige om, mens at dividere med vægtsummen trækker kanten mod de usynlige pixels
// Formen af den indre akkumuleringsløkke, pr. bidragende kildeprøve
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;

// ... og ved destinationspixel, af-præmultiplikér mod alphasummen
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;

Holde 1-bit line art uden for gråzonen

Enhver kontinuert kerne anvendt på en bilevel-scan frembringer grå, og grå er præcis, hvad et fax-stil-billede ikke må indeholde. PDFiumPas lader derfor 1-bit-billeder i fred som standard: PreserveBilevel er True i TPdfImageOptimizeOptions.Default, og sådanne billeder lander urørte i SkippedCount. Sæt den til False, og pirfBilevel-stien overtager i stedet for en udjævningskerne. Den går det præcise kilderektangel igennem, der dækker hver destinationspixel, gennemsnitser luminans med vægtene 0,114, 0,587 og 0,299 i BGR-hukommelsesrækkefølge og tærskler resultatet ved 127,5 til en flad 0 eller 255. Intet mellemliggende kan skrives, så kanter forbliver skarpe, og ingen grå halo dannes omkring tynde streger; alfakanalen af en BGRA-kilde gennemsnitssættes normalt, og en BGRx-destination får konstanten 255. Hvis du har brug for de underliggende pixels frem for et mindre dokument, er udtrækning af billeder fra PDF-dokumenter den separate sti

Hvad sker der, når et billede overskrider arbejdshukommelsesbudgettet?

Det efterlades præcis, som det var, og det tælles. MaxWorkingBytes er 64 MiB som standard og håndhæves to gange. Før destinationsbitmap oprettes, afviser PDFiumPas billedet, hvis bredde gange højde gange bytes pr. pixel overskrider budgettet. Efter FPDFBitmap_CreateEx lykkes tjekker den igen med den reelle stride gange højde, fordi række-padding kan skubbe en allokering forbi en grænse, som det naive produkt clearede. Enhver afvisning ødelægger destinationen og returnerer ingenting. Vær klar over den forværring det indebærer: et billede over budget resamples ikke ved lavere kvalitet, og det opdeles ikke i fliser. Originalen bliver i dokumentet, BudgetExceededCount og SkippedCount stiger begge, og en kørsel kan derfor rapportere succes, mens et dokument kun er delvist optimeret. Det er bevidst fail-safe-adfærd, men det betyder, at rapporten ikke er valgfri læsning. En særskilt fejlmåde findes også: billeder, hvis bitmap PDFium slet ikke kan frembringe, som CMYK, JPX, JBIG2 eller maskerede kilder, øger i stedet FailedCount og efterlades ligeledes urørte

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // brug bilevel-arealafstemningen
  Options.ContentClass := piccPhoto;             // tving Lanczos til fotosæt
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // spillerum til store scanninger
  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;

Læs rapporten, før du afskiber filen

TPdfImageOptimizeReport er bygget til at blive diagnosticeret, ikke blot logget. Ved siden af OptimizedCount, SkippedCount og FailedCount eksponerer den én tæller pr. kerne, så BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount og BilevelFilterCount fortæller dig, hvad den adaptive regel reelt konkluderede om dit korpus. Et alt-box-resultat betyder, at formindskelserne var stejle, eller at indholdet blev klassificeret som line art; et alt-Lanczos-resultat på et dokument, du troede var line art, er et tegn på, at ContentClass burde sættes eksplicit. AverageDetailScore er tallet, der sammenlignes med Lanczos-tærsklen på 0,08 ved justering af PreferredQuality, og PeakWorkingBytes viser, hvor stor en del af MaxWorkingBytes kørslen reelt havde brug for. Ugyldige indstillinger fejler højlydt frem for stille: en ikke-positiv TargetDpi, en MinDpiRatio under 1, en PreferredQuality uden for 0-1 eller en ikke-positiv MaxWorkingBytes rejser EPdfError, før nogen side berøres. Og OptimizeImages redigerer kun dokumentet i hukommelsen; hver ændret side committes med FPDFPage_GenerateContent, hvorefter du stadig selv kalder SaveAs. For at se med egne øjne, hvad der ændredes, render før- og efterdokumenterne til bitmaps som beskrevet i konvertering af PDF-sider til JPEG-billeder og sammenlign dem ved fuldt zoom

Adaptiv resampling er en af de funktioner, der er usynlig, når den virker, og genererer supportbilletter, når den ikke gør, og det er derfor, målingen, alpha-håndteringen og hukommelsesbudgettet måtte lande sammen frem for som tre separate forfinelser. Hvis du evaluerer dette til et Delphi-, C++Builder- eller Lazarus-produkt, er den fulde API-overflade og licensdetaljerne på PDFiumPas Delphi PDFium-komponentsiden