Teknisk artikel

Adaptiv bildomsampling av PDF-bilder i Delphi med PDFiumPas

Två klagomål anländer veckan efter att en komprimeringsfunktion skeppats: det skannade avtalet har nu trappstegsformade, luddiga bokstavsformer, och den transparenta logotypen på försättssidan sitter inuti en blek gloria. PDFiumPas svarar på båda på ett ställe. TPdf.OptimizeImages mäter varje bild innan den krymper den, väljer sedan en omsamplingskärna och ackumulerar färg i alphamedveten form

Det var inte alltid så. Före v3.100.0 nedsamplade samma metod varje icke-binära bild med ett fast nearest-neighbour-steg, vilket är precis den algoritm som producerar båda klagomålen: den punktsamplar en källpixel per utdatapixel, och den behandlar RGB:t under en helt transparent pixel som om en läsare någonsin skulle se det. Omskrivningen i v3.100.0 ersätter den enda vägen med fem kärnor, en mätt urvalsregel och en explicit arbetsminnesbudget

Varför får nedsampling skannad text att se trasig ut?

Eftersom punktsampling svarar på fel fråga. När en 300 DPI-skanning ommålas till 150 DPI representerar varje destinationspixel ett två-gånger-två-block av källpixlar, och nearest neighbour behåller en av de fyra och kastar resten. Vilken som överlever beror på avrundning, så en streckkant som var jämnt kantutjämnad i källan blir ett myntkast per pixel. Resultatet är den klassiska aliastrappan längs glyfkanter, plus moiré på halvtonsområden där de kasserade samplen råkade bära mönstret. Det här spelar större roll i en PDF än på skärm eftersom skadan är permanent. En bild-XObject bär sina sampledata tillsammans med /Width, /Height och /BitsPerComponent (ISO 32000-1 §8.9.5), och omsampling skriver om alla tre inuti filen. En dålig zoom i en läsare är en bildruta du kan rita om, och PDFiumPas har separat maskineri för det i renderingscache och zoomprestanda. En dålig nedsampling är ett nytt dokument du lämnar till kunden

Varför nearest-neighbour-nedsampling förstör skannad text i PDFiumPas för Delphi: varje utdatapixel behåller en av fyra källpixlar och kastar resten, vilket ger aliastaggade glyfkanter och moiré, som de fem omsamplingskärnorna ersätter
Punktsampling behåller en källpixel per utdatapixel och kastar de andra tre, vilket är skälet till att PDFiumPas nu erbjuder fem kärnor i stället för en

Hur PDFiumPas mäter detaljrikedom och väljer kärna

PDFiumPas avgör per bild, inte per dokument. Innan kärnan väljs beräknar den ett normaliserat luminansdetaljpoäng från ett begränsat samplingsrutnät: de horisontella och vertikala stegen är (Width + 63) div 64 och (Height + 63) div 64, så en skanning på 12 000 pixlar och en miniatyr på 300 pixlar kostar båda ungefär samma 64-gånger-64-svep. Vid varje samplad position summerar den den absoluta differensen mot grannen till höger och grannen nedanför, över upp till tre kanaler, och dividerar sedan med samplingsantalet gånger 255. Poängen landar mellan 0 och 1, där platta affärsgrafiker sitter nära noll och tät fotografisk textur klättrar

Urvalsstegen körs sedan i fast ordning. Om ResampleFilter är något annat än pirfAdaptive används filtret ordagrant. Annars: 1-bitarsinnehåll tar pirfBilevel; en ContentClass av piccLineArt tar pirfBox; en skalfaktor på 4 eller mer tar också pirfBox, eftersom ett arealmedelvärde vid den reduktionen är både det billigaste och det mest korrekta svaret; piccPhoto, ett detaljpoäng på 0,08 eller högre, eller en PreferredQuality på 0,9 eller högre tar pirfLanczos med sin trelobskärna; en skala på 2 eller mer eller en kvalitet på 0,7 eller högre tar pirfBicubic med radie 2; allt som blir över tar pirfBilinear. Eftersom TPdfImageOptimizeOptions.Default sätter PreferredQuality till 0,85 faller en standardkörning aldrig tillbaka på bilinjär om inte reduktionen är mild och innehållet platt

Hur PDFiumPas väljer omsamplingskärna i Delphi: ett begränsat sextiofyra-gånger-sextiofyra-svep ger ett normaliserat detaljpoäng, sedan dirigerar en fast stege av villkor varje bild till binär-, box-, Lanczos-, bikubiskt eller bilinjärt filter
Detaljpoängen kostar lika mycket på en skanning på 12 000 pixlar som på en miniatyr, och stegen under den stannar vid första matchande villkor
uses
  PDFium;

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

En bild rörs bara när det större av dess horisontella och vertikala placerings-DPI, dividerat med TargetDpi, når MinDpiRatio. Det skyddet finns så att ett 160 DPI-foto riktat mot ett 150 DPI-mål inte omkodas för en sexprocentig vinst som kostar en kvalitetsgeneration. Bilder under MinDimension på någon axel, 8 som standard, hoppas över som ikoner eller linjer

Varför får transparenta logotyper en vit frans?

Eftersom färgen under en helt transparent pixel är godtycklig, och ett vanligt viktat medelvärde låter den rösta. Exportera en logotyp från ett designverktyg och är den osynliga marginalen ofta vit, eller svart, eller vad duken nu var; alfakanalen döljer den, och en rak summa över kärnans fotavtryck blandar genast in den igen i den synliga kanten. PDFiumPas undviker detta genom att ackumulera BGRA-samplar i premultiplicerad form och häva premultiplikationen bara vid destinationspixeln

Konkret adderar varje bidragande sampl channel * alpha * weight till färgackumulatorn, alpha * weight till en alfaackumulator och weight till viktsumman. Destinationsfärgen divideras sedan med alfaackumulatorn i stället för med viktsumman, och det är steget som spelar roll: division med viktsumman skulle dra färgen mot de osynliga pixlarna, medan division med den ackumulerade alfan rekonstruerar den färg som de synliga samplen faktiskt var överens om. Destinationsalphan är en separat storhet, 255 * AlphaSum / WeightSum. Icke-alfa-format dividerar med viktsumman som vanligt, padding-byten i en FPDFBitmap_BGRx-destination skrivs som konstanten 255, och varje kanal kläms in mellan 0 och 255 innan den lagras. Den alfan kommer normalt från en mjukmaskpost i bildordlistan (ISO 32000-1 §11.4), som PDFium redan har sammansatt in i BGRA-bufferten omsamplaren tar emot

Hur PDFiumPas tar bort den vita glorian från transparenta PDF-bilder i Delphi: samplar ackumuleras i premultiplicerad form, och destinationsfärgen divideras med den ackumulerade alfan i stället för viktsumman så att osynliga pixlar inte kan rösta
Division av den premultiplicerade färgen med den ackumulerade alfan rekonstruerar vad de synliga samplen var överens om, medan division med viktsumman drar kanten mot de osynliga pixlarna
// Utformningen av det inre ackumuleringsloopen, per bidragande källsampl
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;

// ... och vid destinationspixeln, häva premultiplikationen mot alfasumman
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;

Att hålla 1-bitars linjekonst utanför gråzonen

Varje kontinuerlig kärna som tillämpas på en binär skanning producerar grått, och grått är just vad en faxliknande bild inte får innehålla. PDFiumPas lämnar därför 1-bitarsbilder ifred som standard: PreserveBilevel är True i TPdfImageOptimizeOptions.Default, och sådana bilder landar orörda i SkippedCount. Sätt den till False och tar pirfBilevel-vägen över i stället för en utjämnande kärna. Den går igenom den exakta källrektangel som täcker varje destinationspixel, medelvärdesbildar luminans med vikterna 0,114, 0,587 och 0,299 i BGR-minnesordning, och trösklar resultatet vid 127,5 till ett platt 0 eller 255. Inget mellanliggande kan skrivas, så kanter förblir skarpa och ingen grå gloria bildas runt tunna streck; alfakanalen i en BGRA-källa medelvärdesbildas normalt, och en BGRx-destination får konstanten 255. Om du behöver de underliggande pixlarna i stället för ett mindre dokument, är att extrahera bilder från PDF-dokument den separata vägen

Vad händer när en bild överskrider arbetsminnesbudgeten?

Den lämnas precis som den var, och den räknas. MaxWorkingBytes är som standard 64 MiB och tillämpas två gånger. Innan destinationsbitmappen skapas avvisar PDFiumPas bilden om bredd gånger höjd gånger byte per pixel överskrider budgeten. Efter att FPDFBitmap_CreateEx lyckats kontrollerar den igen med den verkliga stegdelen gånger höjd, eftersom radutfyllnad kan driva en allokering förbi en gräns som den naiva produkten klarade. Båda avvisningarna förstör destinationen och returnerar ingenting. Var tydlig med den degradering det innebär: en bild över budget omsamplas inte med lägre kvalitet, och den delas inte i brickor. Originalet stannar i dokumentet, BudgetExceededCount och SkippedCount ökar båda, och en körning kan därför rapportera framgång medan ett dokument bara är delvis optimerat. Det är avsiktligt fail-safe-beteende, men det betyder att rapporten inte är valfri läsning. Ett distinkt felläge finns också: bilder vars bitmapp PDFium inte alls kan producera, såsom CMYK-, JPX-, JBIG2- eller maskerade källor, ökar i stället FailedCount och lämnas likaså orörda

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // använd binär arealröstning
  Options.ContentClass := piccPhoto;             // tvinga Lanczos för fotosamlingar
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // spelrum för stora skanningar
  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;

Att läsa rapporten innan du skeppar filen

TPdfImageOptimizeReport är byggd för att diagnostiseras, inte bara loggas. Vid sidan av OptimizedCount, SkippedCount och FailedCount exponerar den en räknare per kärna, så BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount och BilevelFilterCount talar om vad den adaptiva regeln faktiskt kom fram till om din korpus. Ett resultat med enbart box betyder att reduktionerna var branta eller att innehållet klassificerades som linjekonst; ett resultat med enbart Lanczos på ett dokument du trodde var linjekonst är ett tecken på att ContentClass bör sättas explicit. AverageDetailScore är talet att jämföra mot 0,08-tröskeln för Lanczos när du justerar PreferredQuality, och PeakWorkingBytes visar hur mycket av MaxWorkingBytes körningen faktiskt behövde. Ogiltiga alternativ felar högljutt i stället för tyst: ett icke-positivt TargetDpi, ett MinDpiRatio under 1, en PreferredQuality utanför intervallet 0 till 1, eller ett icke-positivt MaxWorkingBytes kastar EPdfError innan någon sida rörs. Och OptimizeImages redigerar bara minnesdokumentet; varje modifierad sida committas med FPDFPage_GenerateContent, varefter du fortfarande anropar SaveAs själv. För att ögonsyna vad som ändrades, rendera dokumenten före och efter till bitmappar enligt beskrivningen i att konvertera PDF-sidor till JPEG-bilder och jämför dem vid full zoom

Adaptiv omsampling är en av de funktioner som är osynliga när de fungerar och genererar supportärenden när de inte gör det, vilket är skälet till att mätningen, alfahanteringen och minnesbudgeten var tvungna att landa tillsammans i stället för som tre separata förfiningar. Om du utvärderar detta för en Delphi-, C++Builder- eller Lazarus-produkt finns hela API-ytan och licensdetaljerna på sidan för PDFiumPas Delphi PDFium-komponenten