Technischer Artikel

HotPDF-Downsampling-Kernel und Druck-Dithering

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

Gewichtskurven der drei HotPDF-Downsampling-Kernel: rkHalftone delegiert an den bilinear-klassigen GDI-HALFTONE-Pfad mit Support 1, rkBicubic fährt ein trennbares Catmull-Rom-Kubik mit Support 2, und rkLanczos3 einen windowed Sinc mit Support 3 — rund eine Größenordnung Geschwindigkeit wird eingetauscht für sichtbar bessere Fotos
Die drei Kernel-Werte liegen an wirklich verschiedenen Punkten der Geschwindigkeits-Qualitäts-Kurve: ein bilinear-klassiger GDI-Pfad, ein Catmull-Rom-Kubik und ein windowed Sinc mit drei Lappen, und die trennbaren Kernel normalisieren die Gewichte, sodass nichts über Schwarz oder Weiß hinaus schwingt

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

Plot des äußeren Zweigs des HotPDF-bicubischen Catmull-Rom, der zeigt, warum ein falscher Koeffizient sich versteckt: Der Tippfehler, der in der verschachtelten Horner-Form -5 statt -4 schreibt, wertet trotzdem aus und lässt W(1) bei -1 und W(2) bei -2, wo null gefordert ist — die Behauptung W(0) = 1 plus die zwei Nullbedingungen findet ihn in Sekunden
Ein kaputter Kernel stürzt nie ab, er liefert nur Zahlen, die plausibel aussehen — deshalb findet das Auge einen Koeffizienten-Tippfehler nicht. W(0) = 1 mit Nullen bei plus und minus eins und zwei ist ein Dreizeiler-Unit-Test

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

Platzierung von HotPDF-Floyd-Steinberg-Dithering in der Render-Pipeline: RenderOutputDither läuft nach der Seitenkomposition auf dem fertigen 24-Bit-Raster, schwellt die Luminanz bei 50 Prozent und diffundiert jeden Quantisierungsfehler mit den Gewichten 7/16, 3/16, 5/16 und 1/16 nach rechts und unten durch einen Zeilenpuffer, der akkumulieren muss — heraus kommt monochrome 1-Bit-Ausgabe
Dithering gehört nach der Komposition, weil es ein fertiges Raster auf ein Bit reduziert, nicht wegen der Skalierung der Bilder. Die Diffusionsgewichte summieren sich zu eins, und der Zeilenpuffer muss akkumulieren statt überschreiben
// 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