Technischer Artikel

Mixed Raster Content in Delphi: Scan als JPEG + CCITT

losLab PDF Library for Delphi komprimiert eine gescannte Seite mit Mixed Raster Content über TPDFlib.CompressPageAsMRC. Die Methode rendert die Seite, erzeugt über einen Schwellwert eine voll aufgelöste 1-Bit-Vordergrundmaske, verkleinert den Rest zu einem JPEG-Hintergrund und setzt die Seite als zwei Bilder wieder zusammen: einen /DCTDecode-Hintergrund und eine /CCITTFaxDecode-Stencil-Maske, gemalt in der aktuellen Füllfarbe

Diese Methode gibt es für die Seite, von der jedes Dokumentarchiv voll ist: ein 300-DPI-Farbscan einer Rechnung oder eines Vertrags, gespeichert als ein einziges großes JPEG. Kodiert man sie mit einer Qualität, die den Text lesbar hält, wird die Datei groß; dreht man die Qualität runter, wachsen um die Buchstaben Ring-Halos und verschmierte Serifen, denn das Schlechteste, was JPEG kann, ist eine harte schwarze Kante auf weißem Papier. Mixed Raster Content hört auf, einen Codec zwei Jobs machen zu lassen

Warum macht ein einzelnes JPEG gescannten Text sowohl unscharf als auch groß?

Ein einzelnes JPEG behandelt gescannten Text schlecht, weil der DCT-Codec seine Bits auf weiche Verläufe verwendet und harte Kanten mit Hochfrequenzkoeffizienten approximiert, die die Quantisierung zuerst wegwirft. Text auf einem Scan besteht fast nur aus harten Kanten. Diese Kanten sauber zu halten erzwingt eine hohe Qualitätseinstellung über die ganze Seite, einschließlich Papiertextur, Logo und Kaffeefleck, die keines davon nötig hätte

Die MRC-Idee teilt die Seite danach auf, was jede Region tatsächlich ist. Tinte wird ein Bilevel-Bild, in dem ein Fax-Codec wie CCITT Folgen schwarzer und weißer Pixel verlustfrei und sehr kompakt aufzeichnet. Alles andere — Papierton, Fotos, farbige Hintergründe — wird ein Continuous-Tone-Bild, das sich mit einem Bruchteil der Auflösung speichern lässt, denn niemand liest einen Hintergrund bei 300 DPI. Die Bilevel-Ebene hält den Text in voller Auflösung knackig; die Hintergrundebene hält die Farbe günstig

Einzelnes JPEG eines Scans gegenüber der Layer-Aufteilung nach Mixed Raster Content, wie sie PDFlibPas CompressPageAsMRC in Delphi erzeugt: der DCT-Codec verschmiert harte Textkanten und erzwingt hohe Qualität über die ganze Seite, während Tinte zu einem verlustfreien 1-Bit-CCITT-Stencil bei 300 DPI wird und der Papierton zu einem 75-DPI-DeviceRGB-JPEG-Hintergrund
Mixed Raster Content hört auf, einen Codec zwei Jobs machen zu lassen: die Bilevel-Ebene hält die Buchstaben in voller Auflösung knackig und der Hintergrund hält die Farbe zu einem Bruchteil der Kosten, sodass Qualität 50 bis 70 meist unsichtbar bleibt

Wie teilt CompressPageAsMRC eine Seite in zwei Ebenen?

TPDFlib.CompressPageAsMRC fährt pro Seite eine feste Pipeline ab. Sie rendert die Seite in der angeforderten DPI-Auflösung in eine 24-Bit-Bitmap, klassifiziert jedes Pixel anhand einer mit Rec. 601 gewichteten Luminanz (299/587/114 Promille) gegen Threshold und schreibt das Ergebnis in eine 1-Bit-Maske derselben Auflösung. Parallel verkleinert sie dieselbe Renderung um BGScale per Boxmittelwert, sodass ein BGScale von 4 aus jedem 4×4-Block von Pixeln ein Hintergrundpixel macht

Datenfluss von CompressPageAsMRC in PDFlibPas für Delphi: ein 300-DPI-24-Bit-Render wird über Rec. 601-Luminanz zu einem voll aufgelösten CCITTFaxDecode-K-1-ImageMask-Stencil geschwellwertet, per Boxmittelwert mit BGScale 4 zu einem DCTDecode-DeviceRGB-Hintergrund verkleinert, und beide Bilder ersetzen die Original-Operatoren der Seite, mit der Maske obenauf
Beide Ebenen stammen aus einer einzigen Renderung: DPI entscheidet, wie feiner Strich die Schwellwertbildung überlebt, BGScale tauscht Hintergrunddetail von 2 für Fotos bis 8 für reinen Text ein, und die neu gebaute Seite zeichnet den Stencil in der aktuellen Füllfarbe

Danach wird der Seiteninhalt ersetzt. Die Original-Operatoren werden entfernt, der verkleinerte Hintergrund wird als /DCTDecode-Bild in /DeviceRGB eingebettet (ISO 32000-1 §7.4.8), die Maske wird mit dem CCITT-Encoder der Bibliothek als /CCITTFaxDecode-Bild (§7.4.6) kodiert und mit /ImageMask true markiert, und beide werden über die volle Seitenbox gezeichnet, mit der Maske obenauf. Die Signatur ist kurz:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page ist 1-basiert und muss innerhalb von PageCount liegen
  • DPI ist die Renderauflösung des Quell-Rasters, 50 bis 1200
  • BGScale ist der Verkleinerungsfaktor des Hintergrunds, 1 bis 8
  • JPEGQuality ist die JPEG-Qualität des Hintergrunds, 1 bis 100
  • Threshold ist die Luminanzschwelle, 1 bis 255; Pixel ab diesem Wert sind Papier, dunklere Pixel sind Tinte
  • Options ist reserviert und muss 0 sein

Ein typischer Batch über eine gescannte Datei sieht so aus. Rufen Sie SetFillColor direkt vor jeder Seite auf, aus einem Grund, den der nächste Abschnitt erklärt

uses
  SysUtils, PDFlibrary;

procedure CompressScannedFile(const SrcFile, DstFile: string);
var
  Lib: TPDFlib;
  P, Replaced: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SrcFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
    for P := 1 to Lib.PageCount do
    begin
      Lib.SetFillColor(0, 0, 0);  // Farbe der Tinte der Stencil-Ebene
      // 300-DPI-Maske, 75-DPI-Hintergrund, JPEG-Qualität 60, Schnitt bei Luma 160
      Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
      if Replaced = 0 then
        raise Exception.CreateFmt('MRC failed on page %d', [P]);
    end;
    if Lib.SaveToFile(DstFile) <> 1 then
      raise Exception.CreateFmt('Cannot save %s', [DstFile]);
  finally
    Lib.Free;
  end;
end;

Warum wird der Vordergrund in der aktuellen Füllfarbe gemalt?

Die Vordergrundebene ist eine Stencil-Maske, und eine Stencil-Maske hat keine eigene Farbe. ISO 32000-1 §8.9.6.2 definiert ein Bild mit /ImageMask true als 1-Bit-Form: Die Sample-Werte markieren, wohin Farbe geht, und die Farbe ist der aktuelle Nonstroking-Farbwert im Grafikzustand. Genau das macht die Ebene billig, und genau deshalb nimmt CompressPageAsMRC keinen Farbparameter entgegen

TPDFlib.CompressPageAsMRC malt die Maske mit der durch SetFillColor gesetzten Farbe, daher ist die Aufrufreihenfolge entscheidend. Der Painter emittiert die Füllfarbe erst spät: SetFillColor merkt die Farbe vor und markiert sie als geändert, und der Stencil-Draw schreibt den rg-Operator innerhalb seines eigenen q/Q-Blocks nur, solange diese Änderung noch aussteht. Zeichnen Sie dazwischen eine gefüllte Form, wird die ausstehende Änderung von dieser Form konsumiert statt von der Maske. Setzen Sie die Farbe direkt vor jeder Seite wie im Batch oben, gehört das Rätselraten der Vergangenheit an. Schwarz ist die normale Wahl; ein Dunkelblau funktioniert bei Formularen in blauer Tinte, wenn das Ergebnis weniger wie eine Fotokopie aussehen soll

Sequenz der späten Füllfarb-Emission für die PDFlibPas-MRC-Stencil-Ebene in Delphi: SetFillColor merkt die Farbe als ausstehende Änderung vor, der ImageMask-Stencil-Draw schreibt den rg-Operator innerhalb seines eigenen q- und Q-Blocks, solange die Änderung aussteht, und eine dazwischenliegende gefüllte Form konsumiert die ausstehende Änderung stattdessen
Eine Stencil-Maske besitzt keine eigene Farbe, also nimmt jedes Tintenpixel eine einzige Füllfarbe an: ein roter Stempel, eine blaue Unterschrift und schwarzer Text fallen in eine Farbe zusammen, weshalb SetFillColor direkt vor jeder Seite aufgerufen werden muss
// Blaues Kugelschreiber-Formular: die Tintenschicht in Dunkelblau rendern
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

Die Platzierung folgt dem Koordinatenursprung der Bibliothek. CompressPageAsMRC liest GetOrigin und zeichnet beide Bilder von der Oberkante der Seitenbox: Top-Koordinate 0, wenn der Ursprung oben links liegt (SetOrigin(1)), Top-Koordinate PageHeight beim voreingestellten Ursprung unten links. Anpassen müssen Sie nichts, aber es erklärt, warum die Ebenen korrekt landen, welchen Ursprung Ihre Pipeline auch immer bereits verwendet

Eine Konsequenz der Stencil-Semantik sollte klar ausgesprochen werden: Jedes als Tinte klassifizierte Pixel nimmt dieselbe Farbe an. Ein roter Stempel, eine blaue Unterschrift und schwarzer Fließtext landen alle in einer Füllfarbe auf der Vordergrundebene. Nur das Hintergrund-JPEG bewahrt darunter eine schwache, niedrig aufgelöste Spur der Originalfarben

Wahl von DPI, BGScale, Threshold und JPEG-Qualität

Der Wert DPI entscheidet über die Schärfe des Texts, und 300 ist die vernünftige Voreinstellung für Büro-Scans. Die Maske wird in voller Renderauflösung gehalten, also legt DPI fest, wie feiner Strich die Schwellwertbildung überlebt. Über die ursprüngliche Scan-Auflösung hinauszugehen bringt nichts, denn der Renderer würde nur das eingebettete Scan-Bild hochskalieren. Speicher ist das zweite Limit: Eine US-Letter-Seite bei 300 DPI rendert zu 2550×3300 Pixeln, 25.245.000 Byte an 24-Bit-Pixeldaten, bevor Maske und Hintergrund abgeleitet sind, und dieselbe Seite an der 1200-DPI-Obergrenze sind 10200×13200 Pixel, rund 404 MB

BGScale tauscht Hintergrunddetail gegen Größe. Bei 300 DPI ergibt Faktor 4 einen 75-DPI-Hintergrund, 638×825 Pixel für Letter, was für Papierton und einfarbige Flächen reichlich ist. Seiten mit Fotos oder Rasterillustrationen, die ein Leser sich tatsächlich ansieht, verdienen 2; reiner Text auf weißem Papier verträgt 6 oder 8, da der Hintergrund kaum mehr als ein Farbton ist

Threshold ist der Parameter, der das Aussehen der Seite verändert. Ihn anzuheben klassifiziert mehr mittelgraue Pixel als Tinte, was dünne Striche und Bleistiftmarken verdickt, aber auch Scanner-Rauschen und Papierschatten als Punkte einsammelt. Ihn abzusenken liefert saubereres Papier und dünnere, manchmal abgerissene Buchstaben. Werte um 128 bis 170 sind eine vernünftige Startrange für schwarzen Text auf Weiß; der Trade-off ist derselbe wie in Rendern von PDF-Seiten in 1-Bit-Monochrom in Delphi besprochen, wo dieselbe Luminanzschwelle RenderPageToMonochromeFileEx steuert. Anders als diese Methode hat CompressPageAsMRC keinen Despeckle-Durchlauf, daher fährt man mit verrauschten Scans mit einer etwas niedrigeren Schwelle besser

JPEGQuality wiegt weniger als bei einem Einzelbild-Scan, weil der Hintergrund keine Textkanten mehr trägt. Qualität 50 bis 70 fällt auf einem 75-DPI-Hintergrund hinter einer scharfen Maske meist nicht auf

Was verrät der Rückgabewert, und was nicht?

TPDFlib.CompressPageAsMRC gibt die Größe in Byte des ersetzten Seiteninhalts zurück, und 0, wenn etwas schiefging. Er ist ein Erfolgsindikator, kein Maß für die Ersparnis. Auf einer typischen gescannten Seite ist der Original-Content-Stream nur ein paar Dutzend Byte — gerade genug Operatoren, um ein Bild zu platzieren —, also ist eine kleine positive Zahl das normale Ergebnis, selbst wenn die Seite um Megabyte schrumpft. Eine Seite, deren ursprünglicher Content-Stream leer war, meldet 0, obwohl sie neu gebaut wurde

Das Fehlerverhalten ist nicht atomar, und genau daran muss man sein Design ausrichten. Argumentprüfungen, das Rendern und die JPEG-Kodierung laufen, bevor die Seite berührt wird, sodass ein außerhalb des Bereichs liegendes BGScale, ein JPEGQuality von 101 oder ein Renderfehler die Seite exakt unverändert lässt. Sobald der Originalinhalt aber gelöscht ist, liefert auch ein Fehlschlag beim Einbetten oder Zeichnen einer der Ebenen 0 zurück, und zu diesem Zeitpunkt kann die Seite bereits teilweise neu gebaut sein. Behandeln Sie jede 0 als Grund, das Dokument im Speicher zu verwerfen statt es zu speichern, und schreiben Sie das Ergebnis niemals über die Quelldatei

Um herauszufinden, was die Konvertierung tatsächlich gespart hat, messen Sie das Dokument. Die Länge von SaveToString vor und nachher zu vergleichen ist die schnellste Prüfung; für eine Antwort pro Kategorie zeigt das PDF-Dateigrößen-Audit mit AuditDocumentSpace, ob das Gewicht wie erwartet aus den Bildern gewandert ist

function MRCSavings(Lib: TPDFlib): Int64;
var
  Before: Int64;
  P: Integer;
begin
  Before := Length(Lib.SaveToString);
  for P := 1 to Lib.PageCount do
  begin
    Lib.SetFillColor(0, 0, 0);
    if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
      raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
  end;
  Result := Before - Length(Lib.SaveToString);
end;

Wo dieses Zwei-Ebenen-MRC hinter ITU-T T.44 zurückbleibt

CompressPageAsMRC implementiert ein bewusst vereinfachtes MRC. Das vollständige Modell in ITU-T T.44 verwendet drei Ebenen pro Stripe: ein Hintergrundbild, ein Vordergrundbild, das die Tintenfarben trägt, und eine Bilevel-Maske, die pixelweise zwischen ihnen wählt, mit Raum für mehrere Masken- und Vordergrundpaare auf einer Seite. In PDF-Begriffen wäre die dritte Ebene ein Farb-Vordergrundbild in niedriger Auflösung mit expliziter Maske (§8.9.6.3) statt eines in einer Farbe gemalten Stencils. PDF Library for Delphi lässt die Vordergrundfarbebene weg, was das Ergebnis so klein macht und auch der Grund ist, warum farbige Tinte in der Füllfarbe zusammenfällt

  • Die Methode passt zu schwarzem oder einfarbigem Text auf gescanntem Papier; farbcodierte Dokumente, markierter Text und farbige Stempel verlieren ihre Tintenfarbe
  • Die neu gebaute Seite ist reines Raster: echter Text, Vektorgrafik oder selektierbare Zeichen auf der Originalseite werden zu Pixeln, führen Sie es also auf Scans aus, nicht auf digital erzeugten Seiten, wo Font-Subsetting und Bild-Downsampling die richtigen Werkzeuge sind
  • Die Maske verwendet den CCITT-Encoder der Bibliothek im T.4-Zweidimensionalmodus, geschrieben als /CCITTFaxDecode mit /K 1; der Encoder hat auch einen Group-4-Modus über AddImageCCITTFromBitmapStream, aber CompressPageAsMRC legt ihn nicht offen und verwendet kein JBIG2
  • Annotationen und Formularfelder werden nicht eingeebnet; sie bleiben als Objekte über dem neu gebauten Inhalt erhalten
  • Der Hintergrund ist /DeviceRGB, also eignet sich die Ausgabe nicht für PDF/X-1a-Workflows, und SetFillColor selbst wird im PDF/X-1a-Modus abgewiesen
  • Ein Scan, der ohnehin OCR braucht, sollte vor MRC erkannt werden, solange die Originalpixel noch in voller Farbtiefe existieren

FAQ

Bleibt der Text mit CompressPageAsMRC durchsuchbar?

Nein. CompressPageAsMRC ersetzt den Seiteninhalt durch zwei Bilder, also werden alle Text-Operatoren auf der Originalseite — einschließlich einer unsichtbaren OCR-Textebene — mit entfernt. Führen Sie OCR hinterher auf der komprimierten Seite aus oder fügen Sie die Textebene nach der Komprimierung wieder hinzu

Kann ich nur einige Seiten eines Dokuments komprimieren?

Ja. CompressPageAsMRC arbeitet pro Aufruf auf einer 1-basierten Seite, Sie wählen die Seiten also selbst aus und können BGScale und Threshold sogar pro Seite variieren, etwa Faktor 2 für einen Fotoeinschub und 6 für reine Textseiten

Was soll der Options-Parameter sein?

Übergeben Sie 0. Options ist in der aktuellen Version für die Zukunft reserviert und es sind keine Bits definiert, ein Wert ungleich null trägt heute also keine Bedeutung

CompressPageAsMRC kam in losLab PDF Library for Delphi v3.561.0 zusammen mit den Render-, JPEG-, CCITT- und Stencil-Masken-Bausteinen, aus denen es zusammengesetzt ist; alle sind als öffentliche Methoden verfügbar, falls Sie eine Variante der Pipeline brauchen. Die Bibliothek, ihre C++Builder-Unterstützung und die vollständige API-Referenz finden Sie auf der Produktseite der PDF Library for Delphi