Ein mit /Predictor 12 markierter Stream bedeutet nicht, dass jede Zeile PNG-Filter 2 verwendet. HotPDF, die native VCL-PDF-Komponente für Delphi und C++Builder, behandelt Predictor-Werte 10 bis 15 als eine Familie: das eigentliche Filter-Tag, 0 bis 4, ist das erste Byte jeder kodierten Zeile, und HPDFDecodePredictor liest und validiert dieses Tag Zeile für Zeile. Diese Unterscheidung ist die Form fast jedes Bugs in dieser Ecke von PDF, denn nichts schlägt Alarm, wenn man es falsch macht. Die Filterkette läuft, das Raster hat die erwartete Größe, und das Bild kommt als diagonales Rauschen oder als Verlauf heraus, der mit jeder Scanzeile weiter abdriftet. Die fünf Zahlen in /DecodeParms (ISO 32000-1 §7.4.4) ändern meist die Bedeutung der Bytes, nicht ihre Länge, sodass ein falscher Wert plausiblen Datenmüll statt eines Fehlers erzeugt
Warum bedeutet /Predictor 12 nicht PNG-Filter 2 auf jeder Zeile?
Weil die Predictor-Nummer nur sagt "PNG-Prädiktion ist im Einsatz", nicht welcher Filter. PNG-Encoder wählen einen Filter pro Scanzeile, und der PDF-Filter erbt das, sodass die Predictor-Werte 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) und 15 (Optimum) alle identisch dekodieren: Das führende Tag-Byte jeder Zeile ist es, dem der Decoder gehorchen muss. Die Layout-Konsequenz ist ebenso wichtig wie die Semantik. Jede kodierte Zeile ist 1 + RowBytes Bytes lang, die Eingabe übersteigt also die Ausgabe um genau die Zeilenzahl, und ein Stream, dessen Länge kein ganzzahliges Vielfaches von RowBytes + 1 ist, ist per Definition abgeschnitten. HotPDF prüft diese Grenze, bevor es ein Byte anfasst, weist jedes Tag über 4 mit Invalid PNG predictor row tag zurück und liest die vorherige Zeile direkt aus dem einzigen Ausgabepuffer, statt ein zweidimensionales Zeilen-Array zu materialisieren. Filter 1 und 3 greifen BytesPerPixel weit in die aktuelle Zeile zurück, Filter 2 liest direkt nach oben, Filter 4 führt die Paeth-Wahl über links, oben und oben-links aus – und alle vier operieren auf bereits rekonstruierter Ausgabe, weshalb die Zeile darüber die dekodierte Zeile sein muss und niemals die gefilterte Eingabe
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
Das Argument MaxOutputBytes ist keine Dekoration. Eine Predictor-Stufe ist eine getarnte Dekompressionsstufe, und ein feindlicher oder schlicht kaputter /Columns-Wert verwandelt ein paar Kilobyte Eingabe in eine Anforderung nach mehreren Gigabyte Speicher. HotPDF berechnet Bits pro Zeile, Bytes pro Zeile und die Gesamtrastergröße zunächst in Int64, verweigert eine Geometrie, die überläuft, und beachtet die vom Aufrufer vorgegebene Obergrenze. Übergibt man eine echte, aus dem Bild-Dictionary abgeleitete Grenze, wird der Fehlschlag zu einer geloggten Meldung statt zu einem Out-of-Memory-Dialog auf einer Kundenmaschine
Warum beschädigt TIFF-Predictor 2 4-Bit-Bilder?
Weil Predictor 2 horizontale Differenzbildung pro Sample ist, nicht pro Byte, und bei 1, 2 oder 4 Bit pro Komponente teilen sich mehrere Samples ein Byte. Die verbreitete Implementierung addiert Byte N-Colors zu Byte N, was bei 8 Bit pro Komponente zufällig korrekt ist und überall sonst still falsch. Ein 8-Bit-RGB-Scan dekodiert einwandfrei, und derselbe Code zerstört dann ein 4-Bit-Indexbild, sobald eines in der Produktion auftaucht
Die korrekte Arithmetik arbeitet innerhalb des Bitfelds. HotPDF durchläuft Samples von Index Colors bis Colors * Columns - 1, extrahiert das Sample und seinen linken Nachbarn derselben Komponente mit einer Maske von (1 shl BitsPerComponent) - 1 bei der passenden Verschiebung, addiert sie modulo dieser Maske und schreibt das Ergebnis zurück, ohne die übrigen im selben Byte gepackten Samples zu stören. Auch das Ende ist wichtig: Eine Zeile wird auf eine Byte-Grenze aufgefüllt, sodass die Füllbits nach dem letzten Sample unangetastet erhalten bleiben müssen, statt in die Arithmetik einbezogen zu werden. Bei 16 Bit pro Komponente ist jedes Sample ein Big-Endian-Bytepaar, und die Addition läuft über $FFFF hinweg über das Paar herum, statt unabhängig zwischen Bytes überzulaufen; bei 8 Bit ist die einfache Byte-Rekurrenz richtig, mit Schrittweite Colors, sodass Rot gegen Rot und Alpha gegen Alpha akkumuliert. In jeder Variante ist das erste Pixel einer Zeile ein Literal, nie eine Differenz, und die Rekurrenz startet an jeder Zeilengrenze neu – TIFF-Prädiktion liest niemals die Zeile darüber, was der gesamte Unterschied zur PNG-Familie ist
Was steuert EarlyChange in LZWDecode wirklich?
Es steuert, wann der Reader seine Codegröße um ein Bit erweitert, und ein Code außer Takt zu sein korrumpiert alles Folgende. HotPDF drückt die Regel als eine einzige Invariante aus: Nach dem Hinzufügen eines Wörterbucheintrags erweitert der nächste Lesevorgang, sobald NextCode (1 shl CodeSize) - Ord(EarlyChange) erreicht. Mit /EarlyChange 1, der Vorgabe aus ISO 32000-1 §7.4.4, geschieht der Wechsel einen Code zu früh; mit /EarlyChange 0 geschieht er exakt an der Grenze. Beide kommen in echten Dateien vor, und nichts im Bitstream verrät, welchen der Encoder verwendet hat. Der Rest der Zustandsmaschine muss im Gleichschritt mitziehen: Ein Clear-Code setzt Codegröße, Bitmaske, nächsten freien Code und den Phrasenspeicher gemeinsam zurück, und der End-of-Information-Code wird mit der zu diesem Zeitpunkt aktuellen Breite gelesen, nicht mit den anfänglichen 9 Bit. HotPDF beginnt bei InitialCodeSize 9, deckelt die Codegröße bei 12 und das Wörterbuch bei 4096 Einträgen, und setzt FillOrder standardmäßig auf foTop, weil PDF Codes mit dem höchstwertigen Bit zuerst packt – foBottom existiert für TIFF-artige Streams, die das nicht tun
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Die Statistiken existieren zur Triage, nicht zur Zierde. Dekodiert eine Datei zur richtigen Länge, aber mit den falschen Pixeln, verraten PeakCodeSize und DictionaryAdds sofort, ob der Reader je dort erweitert hat, wo der Writer es tat. EarlyChange umschalten, erneut dekodieren, beide vergleichen: Bewegen sich die Zahlen, hat man die Antwort in einem Durchlauf statt im Schritt-für-Schritt-Debuggen eines Bit-Readers
Der KwKwK-Zweig, und wann ein Stream schlicht scheitern sollte
Der eine legale Fall, der illegal aussieht, ist Code = NextCode, und HotPDF behandelt ihn, indem es den Eintrag konstruiert, bevor es ihn emittiert. Ein Encoder kann den Code für eine Phrase emittieren, die er im selben Schritt gerade definiert, was immer dann geschieht, wenn die Eingabe ein Muster der Form K w K w K enthält; der Decoder kann diesen Code nicht nachschlagen, weil er noch nicht existiert, muss also Previous + First(Previous) bilden, als neuen Eintrag hinzufügen und genau den Eintrag emittieren, den er gerade erstellt hat. HotPDF zählt diese Fälle in KwKwKExpansions und prüft gegen, dass der hinzugefügte Code der angeforderte Code ist. Alles oberhalb von NextCode ist Korruption, und dort sollte ein Decoder anhalten statt improvisieren: HotPDF löst aus bei einem zukünftigen Code, bei einem Wörterbuch-Präfix, das außerhalb der Phrasenarena zeigt, bei einem vollen Wörterbuch und bei einem ersten Code, der kein Literal ist. Zwei Strenge-Schalter sind standardmäßig bewusst aus, RequireInitialClear und RequireEndOfInformation, weil zahlreiche produktive PDFs den führenden Clear-Code weglassen oder ohne Terminator enden. Man schalte sie ein, wenn man die eigene Ausgabe validiert, und lasse sie aus, wenn man Dateien aus der freien Wildbahn konsumiert
Wo /DecodeParms auf der geladenen Dokumentseite tatsächlich gelesen wird
HotPDF löst /DecodeParms oder seine Abkürzung /DP im Bild-Stream-Dictionary auf, akzeptiert sowohl ein Dictionary als auch ein Array und nimmt das letzte Element, wenn es ein Array ist, dann trägt es Predictor, Colors, BitsPerComponent, Columns und EarlyChange in den Raster-Pfad. Der Array-Fall ist der, den man leicht vergisst: Ein durch [/ASCII85Decode /FlateDecode] gefilterter Stream trägt ein paralleles Parameter-Array, und die Predictor-Einstellungen gehören zum letzten Filter, nicht zum ersten
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Ein historischer Defekt auf diesem Pfad ist es wert, benannt zu werden, weil sich die Fehlerklasse wiederholt. Die alte Flate-mit-Parametern-Routine erzeugte einen Dekompressionsstream und kopierte dann aus der ursprünglichen komprimierten Eingabe, sodass die Predictor-Stufe komprimierte Bytes erhielt und sie brav "entprädiziert": stets falsch, nie ausgelöst. Der aktuelle Code liest nur aus dem Decoder, bevor er das Ergebnis an den gemeinsamen Predictor übergibt, und er weist ein Raster zurück, das kürzer ist als die berechnete Größe, statt auf die noch komprimierten Bytes zurückzufallen – ein Fallback, der früher einen Dekodierfehlschlag in eine korrupte Bitmap verwandelte. Dieselbe Predictor-Implementierung bedient nun auch Cross-Reference-Streams, was ein nützlicher Gleichklang ist, wenn man auch mit Object Streams und Incremental Updates arbeitet, und die umgebende Extraktionsmaschinerie wird im begleitenden Beitrag zum Extrahieren geladener Bilder und ihrer Decode-Filter behandelt. Als DCTDecode oder JPXDecode eintreffende Bilder erreichen den Predictor überhaupt nie; sie tragen ihr eigenes komprimiertes Pixelmodell
Durchsatz: eine zusammenhängende Phrasenarena gegenüber String pro Eintrag
Das Ersetzen des String-pro-Eintrag-Wörterbuchs durch eine zusammenhängende Phrasenarena maß rund das 1,61-Fache an Geschwindigkeit bei einer pathologischen Eingabe: 1558 MiB/s gegenüber 969 MiB/s bei einem Benchmark, dessen einzelne längste Phrase 7.370.880 Bytes erreicht. Die Form dieser Eingabe erklärt den Abstand, denn die klassischen Implementierungen wählen einen von zwei schlechten Tauschgeschäften. Ein Wörterbuch aus AnsiString-Werten alloziert und kopiert für jeden von bis zu 4096 Einträgen einen frischen String, wobei jeder neue Eintrag seinen Elterneintrag vollständig kopiert; ein Präfix/Suffix-Stack vermeidet diesen Speicheraufwand, rekonstruiert aber jede Phrase, indem er die Kette rückwärts Byte für Byte durchläuft und umkehrt, was für gewöhnlichen Text passt und schmerzhaft ist, wenn eine Phrase Megabyte erreicht. HotPDF hängt jede Phrase zusammenhängend an eine geometrisch wachsende Arena an, indiziert Einträge über Offset und Länge und emittiert eine Phrase mit einem einzigen Move in den Ausgabepuffer. Die ehrlichen Kosten sind Speicher: Eine Arena, die jede Phrase vollständig hält, ist durch die Summe aller Phrasenlängen begrenzt, nicht durch die Eintragsanzahl, weshalb MaxOutputBytes sowohl beim Dekompressor als auch beim Predictor existiert. Leitet man diese Grenze aus dem ab, was das Bild-Dictionary über das erwartete Raster behauptet, scheitert ein lügender Stream schnell
Der LZW-Dekompressor, der gemeinsame Predictor und der hier gezeigte Pfad zum Extrahieren geladener Bilder sind Teil der Standard-HotPDF Component für Delphi und C++Builder, mit der vollständigen Filter- und DecodeParms-Referenz auf der Produktseite