Technisch artikel

Adaptieve PDF-beeldresampling in Delphi met PDFiumPas

Twee klachten komen binnen in de week nadat een compressiefunctie verschijnt: het gescande contract heeft nu trappetjesvormige, harige lettervormen, en het transparante logo op de omslagpagina zit in een bleke halo. PDFiumPas beantwoordt beide op één plek. TPdf.OptimizeImages meet elk beeld voordat hij hem verkleint, kiest daarna een resampling-kernel en accumuleert kleur in alpha-bewuste vorm

Dat was niet altijd waar. Vóór v3.100.0 verkleinde dezelfde methode elk niet-bilevel beeld met een vaste nearest-neighbour-stap, precies het algoritme dat beide klachten veroorzaakt: hij neemt per uitvoerpixel één bronpixel als puntmonster, en behandelt de RGB die onder een volledig transparante pixel zit alsof een lezer hem ooit zou zien. De herschrijving in v3.100.0 vervangt dat ene pad door vijf kernels, een gemeten selectieregel en een expliciet werkgeheugenbudget

Waarom ziet gescande tekst er na verkleining verscheurd uit?

Omdat puntmonsters de verkeerde vraag beantwoorden. Wanneer een scan van 300 DPI wordt omgezet naar 150 DPI, staat elke bestemmingspixel voor een blok van twee bij twee bronpixels, en nearest neighbour houdt er één van de vier vast en gooit de rest weg. Welke overleeft hangt af van afronding, dus een veegrand die in de bron glad ge-antialiased was wordt een muntworp per pixel. Het resultaat is de klassieke trapvormige aliasing langs glyph-randen, plus moiré op rasterregio's waar de weggegooide monsters toevallig het patroon droegen. Dit weegt zwaarder in een PDF dan op scherm omdat de schade permanent is. Een image XObject draagt zijn sampledata naast /Width, /Height en /BitsPerComponent (ISO 32000-1 §8.9.5), en resampling herschrijft alle drie binnen het bestand. Een slechte zoom in een viewer is een frame dat u opnieuw kunt tekenen, en PDFiumPas heeft aparte machinerie daarvoor in render cache en zoomprestaties. Een slechte verkleining is een nieuw document dat u aan de klant geeft

Waarom nearest-neighbour-verkleining gescande tekst bederft in PDFiumPas voor Delphi: elke uitvoerpixel houdt één van vier bronpixels vast en gooit de rest weg, wat aliasing-randen bij glyphs en moiré produceert, die de vijf resampling-kernels vervangen
Puntmonsters houden per uitvoerpixel één bronpixel vast en gooien de andere drie weg, en daarom biedt PDFiumPas nu vijf kernels in plaats van één

Hoe PDFiumPas detail meet en een kernel kiest

PDFiumPas beslist per beeld, niet per document. Voordat hij een kernel kiest, berekent hij een genormaliseerde luminantie-detailscore uit een begrensd samplingraster: de horizontale en verticale stappen zijn (Width + 63) div 64 en (Height + 63) div 64, dus een scan van 12000 pixels en een thumbnail van 300 pixels kosten ongeveer dezelfde veeg van 64 bij 64. Op elke gesamplede positie somt hij de absolute differentie op tot de buur rechts en de buur eronder, over maximaal drie kanalen, en deelt daarna door het sampleaantal maal 255. De score landt tussen 0 en 1, waar platte bedrijfsgraphics nabij nul zitten en dichte fotografische textuur klimt

De selectieladder draait daarna in vaste volgorde. Is ResampleFilter iets anders dan pirfAdaptive, dan wordt dat filter letterlijk gebruikt. Anders: 1-bit inhoud neemt pirfBilevel; een ContentClass van piccLineArt neemt pirfBox; een schaalfactor van 4 of meer neemt ook pirfBox, want bij die reductie is een vlakgemiddelde zowel het goedkoopste als het meest correcte antwoord; piccPhoto, een detailscore van 0,08 of hoger, of een PreferredQuality van 0,9 of hoger neemt pirfLanczos met zijn drie-kwab-kernel; een schaal van 2 of meer of een kwaliteit van 0,7 of hoger neemt pirfBicubic met radius 2; alles wat overblijft neemt pirfBilinear. Omdat TPdfImageOptimizeOptions.Default PreferredQuality op 0,85 zet, valt een standaardrun nooit terug op bilinear tenzij de reductie mild is en de inhoud plat

Hoe PDFiumPas in Delphi een resampling-kernel kiest: een begrensde veeg van 64 bij 64 levert een genormaliseerde detailscore op, waarna een vaste ladder van voorwaarden elk beeld naar het bilevel-, box-, Lanczos-, bicubische of bilineaire filter routeert
De detailscore kost evenveel op een scan van 12000 pixels als op een thumbnail, en de ladder daaronder stopt bij de eerste voorwaarde die matcht
uses
  PDFium;

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

Een beeld wordt alleen aangeraakt wanneer de grotere van zijn horizontale en verticale plaatsings-DPI, gedeeld door TargetDpi, MinDpiRatio bereikt. Die garde bestaat zodat een foto van 160 DPI gericht op een doel van 150 DPI niet opnieuw wordt gecodeerd voor zes procent winst die een generatie kwaliteit kost. Beelden onder MinDimension op een van beide assen, standaard 8, worden overgeslagen als iconen of lijnen

Waarom krijgen transparante logo's een witte rand?

Omdat de kleur onder een volledig transparante pixel willekeurig is, en een gewoon gewogen gemiddelde haar laat meetellen. Exporteer een logo uit een designtool en de onzichtbare marge is vaak wit, of zwart, of wat het canvas ook was; het alphakanaal verbergt haar, en een kale som over de kernvoetafdruk mengt haar onmiddellijk terug in de zichtbare rand. PDFiumPas vermijdt dit door BGRA-samples in gepremultiplieerde vorm te accumuleren en de premultiplicatie pas op de bestemmingspixel ongedaan te maken

Concreet voegt elk bijdragende sample channel * alpha * weight toe aan de kleuraccumulator, alpha * weight aan een alphaaccumulator en weight aan de gewichtssom. De bestemmingskleur wordt daarna gedeeld door de alphaaccumulator in plaats van door de gewichtssom, en dat is de stap die telt: delen door de gewichtssom zou de kleur richting de onzichtbare pixels slepen, terwijl delen door de geaccumuleerde alpha de kleur reconstrueert waarop de zichtbare samples feitelijk overeenkwamen. De bestemmingsalpha is een aparte grootheid, 255 * AlphaSum / WeightSum. Niet-alpha-formaten delen zoals gewoonlijk door de gewichtssom, de paddingbyte van een FPDFBitmap_BGRx-bestemming wordt geschreven als constante 255, en elk kanaal wordt afgeklemd tussen 0 en 255 voordat hij wordt opgeslagen. Die alpha komt normaal uit een soft mask-entry in het beeldwoordenboek (ISO 32000-1 §11.4), die PDFium al heeft gecompositeerd in de BGRA-buffer die de resampler ontvangt

Hoe PDFiumPas de witte halo van transparante PDF-beelden in Delphi verwijdert: samples worden in gepremultiplieerde vorm geaccumuleerd, en de bestemmingskleur wordt gedeeld door de geaccumuleerde alpha in plaats van de gewichtssom zodat onzichtbare pixels niet kunnen meetellen
De gepremultiplieerde kleur delen door de geaccumuleerde alpha reconstrueert waarop de zichtbare samples overeenkwamen, terwijl delen door de gewichtssom de rand richting de onzichtbare pixels sleept
// Vorm van de binnenste accumulatielus, per bijdragende bron-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;

// ... en op de bestemmingspixel, unpremultiply tegen de alphasom
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 lijntekeningen uit de grijze zone houden

Elke continue kernel toegepast op een bilevel-scan produceert grijs, en grijs is precies wat een faxstijl-beeld niet mag bevatten. PDFiumPas laat 1-bit beelden daarom standaard met rust: PreserveBilevel is True in TPdfImageOptimizeOptions.Default, en zulke beelden belanden onaangeroerd in SkippedCount. Zet haar op False en het pirfBilevel-pad neemt het over in plaats van een verzachtende kernel. Hij doorloopt de exacte bronrechthoek die elke bestemmingspixel bestrijkt, gemiddelt luminantie met de gewichten 0,114, 0,587 en 0,299 in BGR-geheugenvolgorde, en klemmet het resultaat bij 127,5 vast tot een kaal 0 of 255. Niets intermediair kan worden weggeschreven, dus randen blijven scherp en er vormt zich geen grijze halo rond dunne strepen; het alphakanaal van een BGRA-bron wordt normaal gemiddeld, en een BGRx-bestemming krijgt de constante 255. Als u de onderliggende pixels nodig hebt in plaats van een kleiner document, is beelden uit PDF-documenten extraheren het aparte pad

Wat gebeurt er wanneer een beeld het werkgeheugenbudget overschrijdt?

Hij blijft precies zoals hij was, en hij wordt geteld. MaxWorkingBytes staat standaard op 64 MiB en wordt twee keer afgedwongen. Voordat de bestemmingsbitmap wordt gecreëerd, weigert PDFiumPas het beeld als breedte maal hoogte maal bytes per pixel het budget overschrijdt. Nadat FPDFBitmap_CreateEx is geslaagd, controleert hij opnieuw met de echte stride maal hoogte, want rijpadding kan een allocatie voorbij een grens duwen die het naïeve product had gehaald. Elke weigering vernietigt de bestemming en geeft niets terug. Wees helder over de degradatie die dit impliceert: een beeld over budget wordt niet hersampled op lagere kwaliteit, en hij wordt niet in tegels gesplitst. Het origineel blijft in het document, BudgetExceededCount en SkippedCount stijgen beide, en een run kan dus succes melden terwijl een document maar gedeeltelijk is geoptimaliseerd. Dat is bewust fail-safe gedrag, maar het betekent dat het rapport geen optionele leesstof is. Er bestaat ook een aparte faalmodus: beelden waarvan PDFium de bitmap helemaal niet kan produceren, zoals CMYK, JPX, JBIG2 of gemaskeerde bronnen, verhogen in plaats daarvan FailedCount en blijven eveneens onaangeroerd

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // gebruik de bilevel-vlakstem
  Options.ContentClass := piccPhoto;             // forceer Lanczos voor fotosets
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // speling voor grote 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;

Het rapport lezen voordat u het bestand uitlevert

TPdfImageOptimizeReport is gebouwd om gediagnosticeerd te worden, niet alleen gelogd. Naast OptimizedCount, SkippedCount en FailedCount stelt hij één teller per kernel bloot, dus BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount en BilevelFilterCount vertellen u wat de adaptieve regel feitelijk concludeerde over uw corpus. Een alles-box-resultaat betekent dat de reducties steil waren of de inhoud als line art werd geclassificeerd; een alles-Lanczos-resultaat op een document waarvan u dacht dat het line art was is een teken dat ContentClass expliciet moet worden gezet. AverageDetailScore is het getal om te vergelijken met de Lanczos-drempel van 0,08 bij het afstemmen van PreferredQuality, en PeakWorkingBytes toont hoeveel van MaxWorkingBytes de run echt nodig had. Ongeldige opties falen luid in plaats van geruisloos: een niet-positieve TargetDpi, een MinDpiRatio onder 1, een PreferredQuality buiten 0 tot 1, of een niet-positieve MaxWorkingBytes gooit EPdfError voordat enige pagina wordt aangeraakt. En OptimizeImages bewerkt alleen het in-memory document; elke gewijzigde pagina wordt vastgelegd met FPDFPage_GenerateContent, waarna u zelf nog SaveAs aanroept. Om te zien wat er veranderde, render de voor- en nadocumenten naar bitmaps zoals beschreven in PDF-pagina's naar JPEG-beelden converteren en vergelijk ze op volledige zoom

Adaptieve resampling is zo'n functie die onzichtbaar is wanneer ze werkt en supporttickets genereert wanneer ze niet werkt, en daarom moesten de meting, de alpha-afhandeling en het geheugenbudget samen landen in plaats van als drie aparte verfijningen. Als u dit evalueert voor een Delphi-, C++Builder- of Lazarus-product, staat de volledige API-oppervlakte en licentiëring op de PDFiumPas Delphi PDFium component-pagina