HotPDF stellt drei Downsampling-Kernel für Bilder über die Eigenschaft ImageDownsampleKernel bereit und einen separaten Floyd-Steinberg-Durchlauf über RenderOutputDither. Der erste steuert, wie Fotos aussehen, nachdem man sie verkleinert hat, um ein Größenbudget zu treffen; der zweite, wie sie aussehen, nachdem eine Seite auf Schwarzweiß reduziert wurde. Keiner von beiden ist standardmäßig an, und beide sind aus demselben Grund Opt-in: Sie kosten echte Zeit
Der Druck, der die Leute hierher führt, ist vertraut: Ein 60-MB-Scan eines Vertrags muss durch ein Mail-Gateway, das alles über 10 MB abweist, oder ein Stapel Auszüge muss auf ein Fax-artiges Monochromgerät, das jedes graue Pixel als Papier oder Toner rendert. Beide Probleme sind Resampling-Probleme, und beide haben eine schnelle Antwort, die schlecht aussieht, und eine langsame, die richtig aussieht
Worin sich die drei Kernel tatsächlich unterscheiden
THPDFResampleKernel hat drei Werte, und sie liegen an wirklich verschiedenen Punkten der Geschwindigkeits-Qualitäts-Kurve. rkHalftone delegiert an den historischen GDI-Pfad StretchBlt mit dem HALFTONE-Modus, der trotz des Namens bilinear-klassige Filterung ist: schnell, ausreichend für Line Art und Screenshots und anfällig für die kratzigen Kanten, die man auf verkleinerten Fotos sofort erkennt. rkBicubic fährt einen trennbaren Catmull-Rom-Kernel, und rkLanczos3 einen trennbaren windowed Sinc mit Drei-Lappen-Support
Beide trennbaren Kernel laufen als zwei Durchgänge, horizontal dann vertikal, mit 6 bis 12 Taps pro Zielpixel in reinem Pascal. Das ist grob eine Größenordnung langsamer als der GDI-Pfad, und genau deshalb bleibt rkHalftone der Default. Bei einem nächtlichen Batch mit Tausenden Seiten ist der Unterschied eine Scheduling-Entscheidung, keine Präferenz. Bei einem einzelnen Dokument, auf das ein Nutzer wartet, ist Lanczos3 fast gratis und sichtbar besser
Zwei Implementierungseigenschaften sind wissenswert, weil sie bestimmen, was die Ausgabe kann und was nicht. Ränder werden per Kantenreplikation geklemmt statt gewrappt oder ausgeblendet, und die Gewichte werden pro Zielpixel normalisiert. Zusammen bedeuten die beiden, dass das Ergebnis nie unter Schwarz oder über Weiß hinausschwingt, sodass der klassische Lanczos-Overshoot-Hof um eine harte Kante nicht als beschnittene Artefakte im kodierten Bild auftaucht
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // vor dem Aufruf setzen
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
Das Argument MinimumSavingsBytes, oben 4096, ist die Guard, die die Operation ehrlich hält. Ein neu kodiertes Bild, das bereits effizient komprimiert war, kann einen größeren Stream erzeugen als das Original, und ein Downsampler, der blind jedes Bild ersetzt, vergrößert gelegentlich die Datei, die er verkleinern sollte. Die Schwelle sagt: Ersetze nur, wenn mindestens so viele Byte gespart werden. PreservedCalibratedImageCount meldet die andere konservative Entscheidung: Bilder, die unangetastet blieben, weil sie einen kalibrierten Farbraum tragen, den Resampling kompromittieren würde
Warum ist ein falscher Polynomkoeffizient so schwer zu entdecken?
Weil ein kaputter Interpolationskernel nicht abstürzt oder wirft, sondern ein Bild erzeugt, das auf eine subtile, niemand zuordenbare Weise falsch aussieht. Der Catmull-Rom-Kernel ist stückweise kubisch, und sein äußerer Zweig in verschachtelter Horner-Form ist ((-0.5t + 2.5)t - 4)t + 2. Schreibt man den mittleren Koeffizienten als -5 statt -4, wertet die Funktion trotzdem aus, liefert weiterhin Zahlen in plausibler Range und erzeugt weiterhin ein Bild
Der Schaden zeigt sich darin, dass W(1) zu -1 auswertet, wo 0 herauskommen muss. Negative Gewichte akkumulieren, die Summe clippt bei null, und das sichtbare Symptom ist ein Gradient, dessen linkes Ende schwarz wird, und eine Stufenkante, die ihre Zwischentöne verliert. Nichts im Fehler zeigt auf ein Polynom. Die Prüfung, die es in Sekunden findet, ist Arithmetik statt Blick: Ein interpolierender Kernel muss W(0) = 1 und W(±1) = W(±2) = 0 erfüllen, und jeder Kernel, der diese drei Punkte verfehlt, hat einen Koeffizientenfehler, Punkt. Auf diese drei Werte in einem Unit-Test bestehen, und die ganze Klasse von Tippfehler-Defekten verschwindet
Floyd-Steinberg-Dithering und sein Platz in der Pipeline
Der Dither-Durchlauf ist ein anderes Problem als Resampling und sitzt an anderer Stelle der Pipeline. RenderOutputDither wendet Floyd-Steinberg-Fehlerdiffusion nach der Seitenkomposition an, die einzige Platzierung, die für eine monochrome Druckvorschau oder einen Fax-artigen Export Sinn ergibt: Die Operation dreht sich darum, ein fertiges Raster auf ein Bit pro Pixel zu reduzieren, nicht darum, wie einzelne Bilder auf dem Weg hinein skaliert wurden
Der Algorithmus selbst ist kurz. Die Luminanz wird bei 50 Prozent geschwellt, und der Quantisierungsfehler wird mit den klassischen Gewichten 7/16, 3/16, 5/16 und 1/16 auf vier Nachbarn diffundiert — nach rechts, unten links, unten und unten rechts. Der Ausgabepixel ist in jedem Kanal 0 oder 255. Was die naive Alternative stattdessen liefert, eine harte Schwelle ohne Diffusion, macht aus einem Foto eine Silhouette und verliert jeden Mittlerton, der den Inhalt getragen hat
// Dithering zur Renderzeit für ein monochromes Preview-Gerät
Pdf.RenderOutputDither := True;
// Oder derselbe Durchlauf auf eine Bitmap, die Sie schon besitzen. Die
// Bitmap muss pf24bit sein; die Funktion liefert False, statt zu raten
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Direkter Kernel-Zugriff, wenn Sie außerhalb der Dokumenten-Pipeline resampeln
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
Es gibt ein Implementierungsdetail in der Fehlerdiffusion, das jeden einmal beißt. Der Fehlerpuffer zwischen den Zeilen muss akkumulieren. Jedes Pixel in der nächsten Zeile erhält Beiträge von drei verschiedenen Pixeln der aktuellen Zeile, den 3/16-, 5/16- und 1/16-Taps, und wenn der Code zuweist statt zu addieren, verwirft jeder Schreibvorgang den vorherigen Beitrag und nur der letzte Tap überlebt. Das Bild sieht trotzdem gedithert aus, genau das macht es schwer zu bemerken, aber die Textur ist falsch und die Tonwiedergabe driftet. Der Test, der es findet, ist quantitativ: Ein gleichmäßiges Mittelgrau dithern und verlangen, dass die Innenabdeckung zwischen 40 und 60 Prozent landet
Welchen Kernel sollte eine Größenreduktions-Pipeline nutzen?
Passen Sie den Kernel daran an, was die Bilder tatsächlich sind, und behandeln Sie Dithering als Gerätefrage statt als Kompressionsfrage. Für fotografische Scans, die ein Größenbudget überstehen müssen, hält rkLanczos3 bei 150 oder 200 DPI den Detailgrad, den Leute bemerken, und schneidet die Pixelzahl um den Faktor vier oder mehr. Für Screenshots, Diagramme und Line Art ist rkHalftone tatsächlich völlig in Ordnung und deutlich schneller, denn diese Bilder haben wenige Tonverläufe zu bewahren. Für einen gemischten Batch, den man nicht einzeln inspizieren kann, ist rkBicubic die vernünftige Mitte: besser als bilinear, grob halb so viele Taps wie Lanczos3
Downsampling ist einer von mehreren Hebeln, und nicht immer der größte. Bilevel-Scans reagieren meist deutlich besser auf den Encoder aus nativer JBIG2-Bilevel-Kompression in Delphi, wo der Gewinn aus Symbolewörterbüchern kommt statt aus Pixelzahlen. Vor der Entscheidung hilft es zu wissen, was tatsächlich in der Datei steckt — dafür ist das Extrahieren von Bildern und ihren Decode-Filtern da: Ein Inventar der Bildobjekte und ihrer bestehenden Kompression sagt Ihnen, ob Resampling überhaupt etwas zu holen hat
Bauen Sie die Vorschaufläche, die das Ergebnis zeigt, greift derselbe Rendering-Pfad aus dem Rendern einer PDF-Seite in eine Bitmap, dort wirkt RenderOutputDither — die geditherte Vorschau und die geditherte Ausgabe kommen also aus einem Codepfad statt aus zwei Implementierungen, die auseinanderdriften
Das große Prinzip hinter beiden Features: Qualitätseinstellungen sollten explizit und umkehrbar sein. HotPDF lässt das historische Verhalten als Default, damit eine bestehende Anwendung ohne Überraschung in Ausgabe oder Timing aktualisiert, und legt die besser aussehenden, langsameren Pfade eine Eigenschaftszuweisung weit entfernt. Beide sind Teil der HotPDF Delphi PDF component, neben der Ressourcenoptimierung und der Rendering-Mechanik, auf der sie aufbauen