Teknisk artikkel

Adaptiv PDF-bilderesampling i Delphi med PDFiumPas

To klager ankommer uken etter at en komprimeringsfunksjon sendes: den skannede kontrakten har nå trappetrinn-formede, lodne bokstavformer, og den transparente logoen på forsiden sitter inne i en blek halo. PDFiumPas svarer på begge på ett sted. TPdf.OptimizeImages måler hvert bilde før det krymper det, og velger deretter en resampleringskjerne og akkumulerer farge i alpha-bevisst form

Det var ikke alltid slik. Før v3.100.0 nedskalerte samme metode hvert ikke-bilevel-bilde med et fast nearest-neighbour-trinn, som er nøyaktig algoritmen som produserer begge klagene: den punkt-sampler én kildepiksel per utdatapiksel, og den behandler RGB-en som sitter under en fullstendig transparent piksel som om en leser noensinne ville sett den. Omskrivningen i v3.100.0 erstatter den ene stien med fem kjerner, en målt utvalgsregel og et eksplisitt arbeidsminnebudsjett

Hvorfor får nedskalering skannet tekst til å se hakket ut?

Fordi punktsampling svarer på feil spørsmål. Når en 300 DPI-skann retargetes til 150 DPI, står hver utdatapiksel for en to-på-to-blokk av kildepiksler, og nearest neighbour beholder én av de fire og forkaster resten. Hvilken som overlever avhenger av avrunding, så en strekkant som var jevnt antialiasert i kilden blir et myntkast per piksel. Resultatet er den klassiske aliasede trappen langs glyfkanter, pluss moare på rasterområder der de forkastede prøvene tilfeldigvis bar mønsteret. Dette betyr mer i en PDF enn på skjerm fordi skaden er permanent. Et bilde-XObject bærer sine prøvedata sammen med /Width, /Height og /BitsPerComponent (ISO 32000-1 §8.9.5), og resampling omskriver alle tre inne i filen. En dårlig zoom i en viser er en ramme du kan tegne på nytt, og PDFiumPas har separat maskineri for det i render cache og zoom-ytelse. En dårlig nedskalering er et nytt dokument du gir kunden

Hvorfor nearest-neighbour-nedskalering ødelegger skannet tekst i PDFiumPas for Delphi: hver utdatapiksel beholder én av fire kildepiksler og forkaster resten, noe som produserer aliaserte glyfkanter og moare, som de fem resampleringskernene erstatter
Punktsampling beholder én kildepiksel per utdatapiksel og kaster de andre tre bort, og det er derfor PDFiumPas nå tilbyr fem kjerner i stedet for én

Slik måler PDFiumPas detalj og velger kjerne

PDFiumPas bestemmer per bilde, ikke per dokument. Før den velger en kjerne beregner den en normalisert luminans-detaljscore fra et avgrenset samplingsnett: de horisontale og vertikale trinnene er (Width + 63) div 64 og (Height + 63) div 64, så en 12000-pikslers skann og en 300-pikslers miniatyr begge koster omtrent samme 64-på-64-sveip. Ved hver samplet posisjon summerer den den absolutte differansen til nabo til høyre og naboen under, på tvers av opptil tre kanaler, og deler deretter på prøveantall ganger 255. Scoren lander i 0 til 1, der flat forretningsgrafikk ligger nær null og tettpakket fotografisk tekstur klatrer

Utvalgsstigen kjører deretter i fast rekkefølge. Hvis ResampleFilter er noe annet enn pirfAdaptive, brukes det filteret ordrett. Ellers: 1-bit-innhold tar pirfBilevel; en ContentClass av piccLineArt tar pirfBox; en skaleringsfaktor på 4 eller mer tar også pirfBox, fordi ved den reduksjonen er et område-gjennomsnitt både det billigste og mest korrekte svaret; piccPhoto, en detaljscore på 0.08 eller høyere, eller en PreferredQuality på 0.9 eller høyere tar pirfLanczos med sin tre-lobe-kjerne; en skala på 2 eller mer eller en kvalitet på 0.7 eller høyere tar pirfBicubic ved radius 2; alt som gjenstår tar pirfBilinear. Siden TPdfImageOptimizeOptions.Default setter PreferredQuality til 0.85, faller en standardkjøring aldri tilbake til bilinear med mindre reduksjonen er mild og innholdet er flatt

Slik velger PDFiumPas en resampleringskjerne i Delphi: et avgrenset seksti-fire-på-seksi-fire-sveip produserer en normalisert detaljscore, deretter ruter en fast stige av betingelser hvert bilde til bilevel-, box-, Lanczos-, bicubic- eller bilinear-filteret
Detaljscoreen koster det samme på en 12000-pikslers skann som på en miniatyr, og stigen under den stopper ved den første betingelsen som 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, 64 MiB budsjett.
    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 bilde berøres bare når det større av dets horisontale og vertikale plasserings-DPI, delt på TargetDpi, når MinDpiRatio. Den vakten finnes slik at et 160 DPI-foto siktet mot et 150 DPI-mål ikke re-encodes for en gevinst på seks prosent som koster en generasjon kvalitet. Bilder under MinDimension på enten akse, 8 som standard, hoppes over som ikoner eller linjer

Hvorfor får transparente logoer en hvit kant?

Fordi fargen under en fullstendig transparent piksel er vilkårlig, og et enkelt vektet gjennomsnitt lar den stemme. Eksporter en logo fra et designverktøy og den usynlige marginen er ofte hvit, eller svart, eller hva lerretet var; alfakanalen skjuler den, og en rett sum over kjernefotavtrykket blander den raskt tilbake i den synlige kanten. PDFiumPas unngår dette ved å akkumulere BGRA-prøver i premultiplisert form og angre premultiplikasjonen bare ved utdatapikselen

Konkret legger hvert bidragende utvalg channel * alpha * weight til fargeakkumulatoren, alpha * weight til en alpha-akkumulator og weight til vektsummen. Målfargen deles deretter på alpha-akkumulatoren i stedet for på vektsummen, og det er trinnet som betyr noe: å dele på vektsummen ville dratt fargen mot de usynlige pikslene, mens å dele på den akkumulerte alphaen rekonstruerer fargen de synlige utvalgene faktisk ble enige om. Mål-alphaen er en separat størrelse, 255 * AlphaSum / WeightSum. Ikke-alpha-formater deler på vektsummen som vanlig, utfyllingsbyten til et FPDFBitmap_BGRx-mål skrives som en konstant 255, og hver kanal clampes inn i 0 til 255 før den lagres. Den alphaen stammer normalt fra en soft mask-oppføring i bildeordlisten (ISO 32000-1 §11.4), som PDFium allerede har blent inn i BGRA-bufferen resampleren mottar

Slik fjerner PDFiumPas den hvite haloen fra transparente PDF-bilder i Delphi: utvalg akkumuleres i premultiplisert form, og målfargen deles på den akkumulerte alphaen i stedet for vektsummen slik at usynlige piksler ikke kan stemme
Å dele den premultipliserte fargen på den akkumulerte alphaen rekonstruerer det de synlige utvalgene ble enige om, mens å dele på vektsummen drar kanten mot de usynlige pikslene
// Formen på den indre akkumuleringsløkken, per bidragende kildeutvalg
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 utdatapikselen, unpremultiply mot alpha-summen
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 utenfor gråsonen

Enhver kontinuerlig kjerne anvendt på en bilevel-skann produserer grått, og grått er presis det et faks-stil-bilde ikke har lov til å inneholde. PDFiumPas lar derfor 1-bit-bilder være i fred som standard: PreserveBilevel er True i TPdfImageOptimizeOptions.Default, og slike bilder lander i SkippedCount uberørt. Sett den til False og pirfBilevel-stien tar over i stedet for en utglattningskjerne. Den går det nøyaktige kilderectangelet som dekker hver utdatapiksel, tar gjennomsnitt av luminans med 0.114-, 0.587- og 0.299-vektene i BGR-minnerekkefølge, og terskler resultatet ved 127.5 til en flat 0 eller 255. Ingenting mellomliggende kan skrives, så kanter forblir skarpe og ingen grå halo dannes rundt tynne streker; alfakanalen til en BGRA-kilde tas gjennomsnitt av normalt, og et BGRx-mål får den konstante 255. Hvis du trenger de underliggende pikslene snarere enn et mindre dokument, er uttrekking av bilder fra PDF-dokumenter den separate stien

Hva skjer når et bilde overstiger arbeidsminnebudsjettet?

Det blir stående nøyaktig som det var, og det telles. MaxWorkingBytes har standard 64 MiB og håndheves to ganger. Før mål-bitmapet opprettes, avviser PDFiumPas bildet hvis bredde ganger høyde ganger byte per piksel overstiger budsjettet. Etter at FPDFBitmap_CreateEx har lykkes sjekker den igjen ved å bruke ekte stride ganger høyde, fordi radutfylling kan dytte en tildeling forbi en grense det naive produktet klarte. Enhver avvisning ødelegger målet og returnerer ingenting. Vær klar over degraderingen dette innebærer: et over-budsjett-bilde resampleres ikke ved lavere kvalitet, og det deles ikke opp i fliser. Originalen blir i dokumentet, BudgetExceededCount og SkippedCount øker begge, og en kjøring kan derfor rapportere suksess mens et dokument bare er delvis optimalisert. Det er bevisst fail-safe-oppførsel, men det betyr at rapporten ikke er valgfri lesing. En distinkt feilmodus finnes også: bilder hvis bitmap PDFium ikke kan produsere i det hele tatt, som CMYK, JPX, JBIG2 eller maskerte kilder, øker FailedCount i stedet og er likeledes uberørte

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // bruk bilevel-områdeavstemmingen
  Options.ContentClass := piccPhoto;             // tving Lanczos for fotosett
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // headroom for store skanninger
  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;

Lese rapporten før du sender filen

TPdfImageOptimizeReport er bygget for å diagnostiseres, ikke bare logges. Ved siden av OptimizedCount, SkippedCount og FailedCount eksponerer den én teller per kjerne, så BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount og BilevelFilterCount forteller deg hva den adaptive regelen faktisk konkluderte om samlingen din. Et all-box-resultat betyr at reduksjonene var bratte eller innholdet ble klassifisert som line art; et all-Lanczos-resultat på et dokument du trodde var line art er et tegn på at ContentClass bør settes eksplisitt. AverageDetailScore er tallet å sammenligne med 0.08 Lanczos-terskelen ved justering av PreferredQuality, og PeakWorkingBytes viser hvor mye av MaxWorkingBytes kjøringen virkelig trengte. Ugyldige alternativer feiler høyt i stedet for stille: en ikke-posittiv TargetDpi, en MinDpiRatio under 1, en PreferredQuality utenfor 0 til 1, eller en ikke-posittiv MaxWorkingBytes kaster EPdfError før noen side berøres. Og OptimizeImages redigerer bare dokumentet i minnet; hver modifiserte side committes med FPDFPage_GenerateContent, hvoretter du fortsatt kaller SaveAs selv. For å se med egne øyne hva som endret seg, render før- og etter-dokumentene til bitmapper som beskrevet i konvertering av PDF-sider til JPEG-bilder og sammenlign dem ved full zoom

Adaptiv resampling er en av de funksjonene som er usynlig når den virker og genererer støttebilletter når den ikke gjør det, og det er derfor målingen, alpha-håndteringen og minnebudsjettet måtte lande sammen snarere enn som tre separate forbedringer. Hvis du vurderer dette for et Delphi-, C++Builder- eller Lazarus-produkt, finnes den fullstendige API-flaten og lisensieringsdetaljene på PDFiumPas Delphi PDFium komponentsiden