PDFlibPas dekodiert TIFF mit einem handgeschriebenen Object-Pascal-Parser statt über eine libtiff-Bindung, und Version 3.534.1 hat genau die Stellen verschärft, an denen dieser Parser Eingaben verweigert. BigTIFF-Magic 43 wird jetzt namentlich abgelehnt, TileOffsets und TileByteCounts werden bereits beim Tag-Parsing zurückgewiesen, und jeder Puffer wird über Int64-Arithmetik unter einer Decode-Grenze von 256 MiB dimensioniert
Der Fehler, den das schließt, zeigt sich nie im Labor. Er zeigt sich als ein seit drei Jahren still laufendes Scan-Gateway, bis ein Kunde ein Geodaten-Archiv oder ein Whole-Slide-Medizinbild hindurchschickt. Die Datei hat einen legitimen TIFF-Header. Sie lässt sich parsen. Heraus kommt eine Seite gestreiften Rauschens oder eine Multi-Gigabyte-Allokation, die den Dienst herunterreißt, und nirgends auf dem Weg wurde die Eingabe je als ungültig erklärt. Das ist die Fehlerform, gegen die man bauen sollte: kein Absturz, sondern eine selbstbewusst gelieferte falsche Antwort
Warum beweisen II oder MM nicht, dass ein klassisches TIFF vorliegt?
Weil sich beide Dialekte den Byte-Order-Marker teilen. Klassisches TIFF und BigTIFF beginnen beide mit II oder MM, und das Feld, das sie tatsächlich unterscheidet, ist die 16-Bit-Magic unmittelbar danach: 42 für klassisches TIFF laut TIFF-6.0-Spezifikation, 43 für BigTIFF mit seinen 64-Bit-Offsets. Ein Loader, der FValidTIFF := PopWord = 42 schreibt, liegt beim klassischen TIFF nicht falsch, aber er presst zwei sehr unterschiedliche Zurückweisungen in einen stummen Boolean, sodass ein BigTIFF von einem umbenannten, abgeschnittenen JPEG nicht mehr zu unterscheiden ist. PDFlibPas trennt die Fälle jetzt und vermerkt jeden einzeln in TPDFTIFF.LastError: ein Header unter vier Bytes, ein ungültiger Byte-Order-Marker, Magic 43 und jeder andere Magic-Wert erzeugen jeweils eigenen Text. Die Bibliothek dekodiert BigTIFF weiterhin nicht, und das offen auszusprechen ist der Punkt. Der Aufrufer erhält den Unterschied zwischen „das ist kein TIFF" und „das ist ein TIFF, dessen 64-Bit-Offset-Layout der eingebaute Decoder nicht implementiert", und das ist der Unterschied zwischen einem Support-Ticket, das mit einer Antwort erledigt ist, und einem, das zu einer Woche Rätselraten wird
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Tiles sind eine andere Geometrie, nicht nur ein weiteres Offsets-Array
PDFlibPas weist gekacheltes TIFF bereits beim Parsen der Tags zurück, bevor irgendwelche Pixeldaten angefasst werden. Die Abkürzung, die den Bug einlädt, ist leicht zu sehen: Tag 324 (TileOffsets) und Tag 325 (TileByteCounts) sind Arrays von Datei-Offsets und Bytezahlen, strukturell identisch mit den Strip-Arrays, sodass es zwei Zeilen kostet, die vorhandenen Strip-Felder auf sie zu zeigen, und sauber kompiliert. Es ist trotzdem falsch. Tiles bilden ein zweidimensionales Gitter mit aufgefüllten Randblöcken, ihrer eigenen Zeilenschrittweite innerhalb jedes Tiles und keinerlei RowsPerStrip-Semantik, wie der Abschnitt zu gekachelten Bildern in TIFF 6.0 ausführt. Füttert man einen Strip-Decoder mit Tile-Payloads, scheitert das also nicht laut. SimpleExtract und CompDecode wandern mit falscher Schrittweite durch die Daten und liefern ein Bild mit richtigen Dimensionen und falschen Pixeln. Der ältere Code verschärft das noch, indem er StripsAreTiles, ColumnsPerTile und RowsPerTile in TTIFFPage mitführt: Tile-Geometrie, aufgezeichnet von einem Decoder ohne Tile-Assembler dahinter. In 3.534.1 werfen die Handler für die Tags 324 und 325 den Tile-Fehler und verlassen die IFD sofort, sodass die Zurückweisung das Wort „tiled" trägt, statt Wochen später als Rendering-Beschwerde aufzutauchen
Eine Dimensionsklemme ist kein Speicherbudget
Breite und Höhe jeweils auf 65.535 zu klemmen ist nötig und bei Weitem nicht ausreichend, denn die Größe, die die Allokation antreibt, ist ein Produkt. RowsPerStrip * Width * SamplesPerPixel kann die 32-Bit-Arithmetik überlaufen lassen, lange bevor eine Seite ihr eigenes Limit erreicht, und selbst ohne Overflow kann sie eine Allokation benennen, die kein Dienst versuchen sollte. PDFlibPas berechnet die Zeilenbytes in Int64 und setzt drei Deckel gleichzeitig durch: 65.535 pro Dimension, 32 Farbkomponenten und 256 MiB dekodierter Bytes
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// innerhalb von TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
Drei Details dort wiegen mehr als die Konstanten selbst. Der Höhe-Test ist als Division statt Multiplikation geschrieben, sodass das überdimensionierte Produkt nie gebildet wird. Ein RowsPerStrip unter 1 oder über der Bildhöhe wird zuerst auf die Höhe normalisiert, was die Ein-Strip-Lesart ist, die TIFF 6.0 ohnehin impliziert, und die verhindert, dass ein feindlicher Tag den Strip-Puffer aufbläht. Und die Routine ist geteilt: ValidatePageForDecode läuft am Ende des Tag-Parsens und erneut beim Einstieg in SimpleExtract und CompDecode, sodass Code, der direkt zu einem Decoder geht, das Budget nicht umgehen kann. Dieselbe Regel befolgt PDFlibPas beim Parsen nicht vertrauenswürdiger PDF-Objektgraphen, denn ein Limit an einer von drei Türen ist kein Limit
Was muss ein Aufrufer prüfen, bevor er PageInfo liest?
Zuerst ValidTIFF prüfen, dann PageCount, und erst danach PageInfo indizieren. Eine abgelehnte Datei kann PageCount auf null lassen, und GetPageInfo beantwortet einen Index außerhalb des Bereichs mit einem uninitialisierten TTIFFPage-Record, sodass ein Fehlerpfad, der auf dem Weg zur Meldung Auflösung oder Sample-Zahlen liest, am Ende Rauschen liest. Version 3.534.1 hat beide Aufrufer innerhalb der Bibliothek repariert: der Bild-Import-Pfad liest XRes und YRes nur innerhalb des gültigen Zweigs, und TPDFlib.GetImagePageCount verlangt ValidTIFF, statt allein auf eine von null verschiedene Seitenzahl zu vertrauen. Weiter unten ist das Options-Argument von AddImageFromFile die 1-basierte Seitennummer für ein Multipage-TIFF, sodass GetImagePageCount vertrauenswürdig sein muss, bevor die Schleife startet, nicht danach. Null Seiten ist jetzt eine echte Antwort im Sinne von „hier ist nichts dekodierbar", kein Zufallsprodukt eines frühen Returns, was am meisten zählt, wenn man Duplex-Scan-Stapel kollationiert und verzahnt und ein still falsch dekodiertes Blatt an der falschen Position landen würde
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // fehlerhafter Header, BigTIFF, Tile-Layout oder über dem Budget
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
Selbst bauen oder libtiff linken?
PDFlibPas behält den eingebauten Decoder, und der Ausschlag gebende Faktor ist die Plattform-Reichweite, nicht die Urheberschaft. Rund 1.873 Zeilen Object Pascal kompilieren überall dort, wo der Compiler hingeht: Win32, Win64, macOS, iOS, Android und FPC unter Linux. libtiff 4.7.1 umfasst rund 30.000 Zeilen C, verteilt auf 34 tif_*.c-Übersetzungseinheiten, und die heute existierenden vorgebauten Objektdateien decken nur Windows ab. Wer sie übernimmt, tauscht vollständige TIFF-Abdeckung gegen eine Liste unterstützter Plattformen, die auf die Maschinen schrumpft, auf denen die C-Toolchain läuft, plus ein Linker-Durchlauf, den noch niemand gegangen ist
Was das kostet, sollte ohne Beschönigung benannt werden. Der eingebaute Decoder beherrscht, was Scan-Dokumentenarbeit real produziert: CCITT Group 3 ein- und zweidimensional, Group 4, LZW, Deflate, PackBits und JPEG-in-TIFF, über die Photometrics WhiteIsZero, BlackIsZero, RGB, Palette und CMYK mit Predictor 1 und 2. Diese Payloads decken sich mit den PDF-Filtern aus ISO 32000-1 §7.4.4 und §7.4.6, weshalb dem TIFF-Frontend in einer Scan-Pipeline so viel Gewicht zukommt. Was es nicht beherrscht, ist BigTIFF, Tiles, Fließkomma-Predictor 3, PixarLog und SGILog, alte JPEG-Kompression 6 und Sub-IFD-Pyramiden. Seit 3.534.1 ist jede dieser Ablehnungen eine namentliche Verweigerung statt eines falschen Bilds, und die Bibliothek führt eine schriftliche Trigger-Liste für die Wiedereröffnung der libtiff-Entscheidung:
- ein Kunde meldet eine BigTIFF-Datei und braucht native Unterstützung statt eines Konvertierungsschritts
- ein Kunde meldet gekacheltes TIFF aus medizinischen, GIS- oder industriellen Quellen und braucht es direkt dekodiert
- ein Kunde meldet TIFF mit Fließkomma-Predictor 3
- eine veröffentlichte Schwachstelle trifft die eingebauten CCITT- oder LZW-Decode-Pfade
- das Cross-Plattform-Argument gilt nicht mehr, entweder weil macOS-, iOS- und Android-Support eingestellt wird oder weil eine wiederverwendbare libtiff-Integration macOS und Linux bereits abdeckt
Die Migration selbst ist eingegrenzt statt hypothetisch: ein USE_LIBTIFF-Conditional würde die öffentliche Oberfläche von TPDFTIFF unangetastet lassen, LoadFromStream über TIFFClientOpen mit Stream-Callbacks führen und den Pascal-Parser als Fallback außerhalb von Windows stehen lassen. Bis einer dieser Trigger tatsächlich auslöst, bringt der Unterhalt zweier Decoder und einer verdoppelten Testmatrix nichts, das ein Kunde spüren würde. Eine Kosten aufzuschieben, deren Fluchtweg längst aufgeschrieben ist, ist etwas anderes als sie zu ignorieren
Wo das eine Pipeline für gescannte Dokumente lässt
Betrachten Sie TPDFTIFF als Tor, nicht als Konverter. Datei laden, ValidTIFF lesen, und wann immer es false ist, LastError wortwörtlich protokollieren, denn dieser String ist inzwischen der kürzeste Weg von einem Feldbericht zur Diagnose. Dateien, die am Tor scheitern, bleiben durch Upstream-Konvertierung reparabel, was heute die praktische Antwort für BigTIFF- und Tile-Quellen ist. Für Eingaben jenseits von TIFF geht PDFlibPas einen eigenen Weg über seine AVIF-, HEIF- und JPEG-XL-Bildeingabe, sodass die Frage, welcher Decoder welches Format besitzt, explizit bleibt statt emergent
Das alles sitzt hinter der gewöhnlichen Bild-API, sodass eine Dokumentenpipeline die engere Grenze bekommt, ohne eine Zeile Aufrufcode zu ändern, über die Seitenzahlprüfung hinaus, die ohnehin dazugehört. Wenn Sie einen nativen TIFF-zu-PDF-Pfad für Delphi oder C++Builder abwägen, sind die vollständige Komponente und ihre Bildbehandlung auf der Seite zur PDF Library for Delphi dokumentiert