Una settimana dopo il rilascio di una funzione di compressione arrivano due reclami: il contratto scannerizzato mostra lettere dal contatto a scaletta e sfrangiato, e il logo trasparente sulla copertina resta intrappolato in un alone pallido. PDFiumPas risponde a entrambi in un solo punto. TPdf.OptimizeImages misura ogni immagine prima di ridurla, quindi sceglie un kernel di ricampionamento e accumula il colore in forma consapevole di alpha
Non è sempre stato così. Prima della v3.100.0 lo stesso metodo riduceva ogni immagine non bilevel con un passo a vicino più prossimo fisso, cioè esattamente l'algoritmo che genera entrambi i reclami: campiona un solo pixel sorgente per ogni pixel di destinazione e tratta l'RGB nascosto sotto un pixel completamente trasparente come se un lettore potesse vederlo. La riscrittura della v3.100.0 sostituisce quel percorso unico con cinque kernel, una regola di selezione misurata e un budget esplicito di memoria di lavoro
Perché il downsampling rende sfrangiato il testo scannerizzato?
Perché il point sampling risponde alla domanda sbagliata. Quando una scansione a 300 DPI viene riportata a 150 DPI, ogni pixel di destinazione rappresenta un blocco due per due di pixel sorgente, e il vicino più prossimo ne conserva uno solo e scarta gli altri. Quale sopravvive dipende dall'arrotondamento, quindi un bordo di tratto antialiasato con continuità nella sorgente diventa un lancio di moneta per ogni pixel. Il risultato è la classica scala aliasata lungo i bordi dei glifi, oltre al moiré nelle zone a mezzitoni dove i campioni scartati portavano per caso il motivo. In un PDF conta più che sullo schermo perché il danno è permanente. Un image XObject trasporta i suoi dati di campione insieme a /Width, /Height e /BitsPerComponent (ISO 32000-1 §8.9.5), e il ricampionamento riscrive tutti e tre dentro il file. Uno zoom errato in un viewer è un fotogramma che puoi ridisegnare, e PDFiumPas ha un meccanismo separato per quello nella pagina su render cache e prestazioni dello zoom. Un downsampling sbagliato è un documento nuovo che consegni al cliente
Come PDFiumPas misura il dettaglio e sceglie un kernel
PDFiumPas decide immagine per immagine, non documento per documento. Prima di scegliere un kernel calcola un punteggio di dettaglio della luminanza normalizzato da una griglia di campionamento limitata: i passi orizzontale e verticale sono (Width + 63) div 64 e (Height + 63) div 64, così una scansione da 12000 pixel e una miniatura da 300 pixel costano all'incirca la stessa scansione 64 per 64. In ogni posizione campionata somma la differenza assoluta col vicino a destra e col vicino sotto, su fino a tre canali, poi divide per il numero di campioni per 255. Il punteggio cade tra 0 e 1, dove la grafica aziendale piatta si tiene vicino allo zero e la texture fotografica densa sale
La scala di selezione poi gira in un ordine fisso. Se ResampleFilter è qualcosa di diverso da pirfAdaptive, quel filtro si usa così com'è. Altrimenti: il contenuto a 1 bit prende pirfBilevel; un ContentClass di piccLineArt prende pirfBox; un fattore di scala di 4 o più prende pirfBox anche lui, perché a quella riduzione una media d'area è insieme la risposta più economica e più corretta; piccPhoto, un punteggio di dettaglio di 0.08 o superiore, o un PreferredQuality di 0.9 o superiore prende pirfLanczos col suo kernel a tre lobi; una scala di 2 o più o una qualità di 0.7 o superiore prende pirfBicubic a raggio 2; tutto il resto prende pirfBilinear. Dato che TPdfImageOptimizeOptions.Default imposta PreferredQuality a 0.85, un'esecuzione predefinita non ricade mai sul bilinear se non quando la riduzione è lieve e il contenuto è piatto
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Predefiniti: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, qualità 0.85, budget 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;
Un'immagine viene toccata solo quando la maggiore tra i DPI di posizionamento orizzontale e verticale, divisa per TargetDpi, raggiunge MinDpiRatio. Quella protezione esiste perché una foto a 160 DPI puntata a un obiettivo di 150 DPI non venga ricodificata per un guadagno del sei per cento che costa una generazione di qualità. Le immagini sotto MinDimension su uno dei due assi, 8 per impostazione predefinita, vengono saltate come icone o righe
Perché i logo trasparenti si prendono una frangia bianca?
Perché il colore sotto un pixel completamente trasparente è arbitrario, e una media pesata semplice lo lascia votare. Esporta un logo da uno strumento di design e il margine invisibile è spesso bianco, o nero, o qualunque fosse la tela; il canale alpha lo nasconde, e una somma diretta sull'impronta del kernel lo rimischia subito nel bordo visibile. PDFiumPas lo evita accumulando i campioni BGRA in forma premoltiplicata e annullando la premoltiplicazione solo al pixel di destinazione
In concreto, ogni campione che contribuisce aggiunge channel * alpha * weight all'accumulatore di colore, alpha * weight a un accumulatore di alpha e weight alla somma dei pesi. Il colore di destinazione viene poi diviso per l'accumulatore di alpha invece che per la somma dei pesi, ed è quel passaggio che conta: dividere per la somma dei pesi trascinerebbe il colore verso i pixel invisibili, mentre dividere per l'alpha accumulato ricostruisce il colore su cui i campioni visibili concordano davvero. L'alpha di destinazione è una quantità separata, 255 * AlphaSum / WeightSum. I formati non alpha dividono per la somma dei pesi come al solito, il byte di riempimento di una destinazione FPDFBitmap_BGRx viene scritto come costante 255, e ogni canale viene limitato tra 0 e 255 prima di essere salvato. Quell'alpha normalmente origina da una voce di soft mask nel dizionario dell'immagine (ISO 32000-1 §11.4), che PDFium ha già compositato nel buffer BGRA che il ricampionatore riceve
// Forma del ciclo interno di accumulo, per ogni campione sorgente che contribuisce
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;
// ... e al pixel di destinazione, annulla la premoltiplicazione contro la somma di alpha
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;
Tenere la line art a 1 bit fuori dalla zona grigia
Qualunque kernel continuo applicato a una scansione bilevel produce grigio, e il grigio è esattamente ciò che un'immagine in stile fax non può contenere. PDFiumPas quindi lascia in pace le immagini a 1 bit per impostazione predefinita: PreserveBilevel è True in TPdfImageOptimizeOptions.Default, e queste immagini finiscono in SkippedCount intoccate. Impostalo a False e il percorso pirfBilevel prende il comando al posto di un kernel di smussatura. Percorre il rettangolo sorgente esatto che copre ogni pixel di destinazione, fa la media della luminanza con i pesi 0.114, 0.587 e 0.299 nell'ordine di memoria BGR, e soglia il risultato a 127.5 in uno secco 0 o 255. Nulla di intermedio può essere scritto, quindi i bordi restano nitidi e nessun alone grigio si forma attorno ai tratti sottili; il canale alpha di una sorgente BGRA viene mediato normalmente, e una destinazione BGRx riceve la costante 255. Se ti servono i pixel sottostanti e non un documento più piccolo, estrarre immagini da documenti PDF è il percorso separato
Cosa succede quando un'immagine supera il budget di memoria di lavoro?
Resta esattamente com'era, e viene contata. MaxWorkingBytes è 64 MiB per impostazione predefinita e viene applicato due volte. Prima che la bitmap di destinazione venga creata, PDFiumPas rifiuta l'immagine se larghezza per altezza per byte per pixel supera il budget. Dopo che FPDFBitmap_CreateEx ha successo verifica di nuovo usando lo stride reale per l'altezza, perché il riempimento di riga può spingere un'allocazione oltre un limite che il prodotto ingenuo avrebbe superato. Uno dei due rifiuti distrugge la destinazione e non restituisce nulla. Sii chiaro sulla degradazione che questo implica: un'immagine oltre budget non viene ricampionata a qualità inferiore, e non viene divisa in tile. L'originale resta nel documento, BudgetExceededCount e SkippedCount aumentano entrambi, e un'esecuzione può quindi riportare successo mentre un documento è ottimizzato solo in parte. È un comportamento fail-safe deliberato, ma significa che il report non è una lettura facoltativa. Esiste anche una modalità di guasto distinta: le immagini la cui bitmap PDFium non può produrre affatto, come CMYK, JPX, JBIG2 o sorgenti con maschera, aumentano invece FailedCount e restano anch'esse intoccate
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // usa il voto d'area bilevel
Options.ContentClass := piccPhoto; // forza Lanczos per i set di foto
Options.MaxWorkingBytes := 256 * 1024 * 1024; // margine per le scansioni grandi
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;
Leggere il report prima di consegnare il file
TPdfImageOptimizeReport è costruito per essere diagnosticato, non solo registrato. Accanto a OptimizedCount, SkippedCount e FailedCount espone un contatore per kernel, così BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount e BilevelFilterCount ti dicono cosa la regola adattiva ha davvero concluso sul tuo corpus. Un risultato tutto box significa che le riduzioni erano ripide o il contenuto è stato classificato come line art; un risultato tutto Lanczos su un documento che credevi fosse line art è un segnale che ContentClass va impostata esplicitamente. AverageDetailScore è il numero da confrontare con la soglia Lanczos di 0.08 quando regoli PreferredQuality, e PeakWorkingBytes mostra quanto di MaxWorkingBytes l'esecuzione ha davvero richiesto. Le opzioni non valide falliscono rumorosamente piuttosto che in silenzio: un TargetDpi non positivo, un MinDpiRatio sotto 1, un PreferredQuality fuori dall'intervallo 0-1, o un MaxWorkingBytes non positivo sollevano EPdfError prima che una pagina venga toccata. E OptimizeImages modifica solo il documento in memoria; ogni pagina modificata viene confermata con FPDFPage_GenerateContent, dopo cui chiami comunque SaveAs tu stesso. Per vedere a occhio cosa è cambiato, renderizza i documenti prima e dopo in bitmap come descritto nella pagina su convertire le pagine PDF in immagini JPEG e confrontali a zoom pieno
Il ricampionamento adattivo è una di quelle funzioni che sono invisibili quando funzionano e generano ticket di supporto quando non lo fanno, ecco perché la misura, la gestione di alpha e il budget di memoria dovevano arrivare insieme e non come tre raffinamenti separati. Se lo stai valutando per un prodotto Delphi, C++Builder o Lazarus, la superficie API completa e i dettagli di licenza sono sulla pagina del componente PDFium PDF per Delphi PDFiumPas