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
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
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;
Pageist 1-basiert und muss innerhalb vonPageCountliegenDPIist die Renderauflösung des Quell-Rasters, 50 bis 1200BGScaleist der Verkleinerungsfaktor des Hintergrunds, 1 bis 8JPEGQualityist die JPEG-Qualität des Hintergrunds, 1 bis 100Thresholdist die Luminanzschwelle, 1 bis 255; Pixel ab diesem Wert sind Papier, dunklere Pixel sind TinteOptionsist 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
// 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
/CCITTFaxDecodemit/K 1; der Encoder hat auch einen Group-4-Modus überAddImageCCITTFromBitmapStream, aberCompressPageAsMRClegt 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, undSetFillColorselbst 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