PDFlibPas decodes TIFF with a hand-written Object Pascal parser rather than a libtiff binding, and version 3.534.1 tightened exactly where that parser refuses input. BigTIFF magic 43 is now rejected by name, TileOffsets and TileByteCounts are refused during tag parsing, and every buffer is sized through Int64 arithmetic under a 256 MiB decode ceiling
The defect this closes never shows up in a lab. It surfaces as a scanning gateway that has run quietly for three years, until a customer routes a geospatial archive or a whole-slide medical image through it. The file has a legitimate TIFF header. It parses. What comes out is a page of striped noise, or a multi-gigabyte allocation that takes the service down, and nothing along the way ever declared the input invalid. That is the failure shape worth engineering against: not a crash, but a wrong answer delivered confidently
Why does II or MM not prove you have a classic TIFF?
Because the byte-order marker is shared by both dialects. Classic TIFF and BigTIFF each open with II or MM, and the field that actually distinguishes them is the 16-bit magic immediately after it: 42 for classic TIFF as defined in the TIFF 6.0 specification, 43 for BigTIFF with its 64-bit offsets. A loader written as FValidTIFF := PopWord = 42 is not wrong about classic TIFF, but it collapses two very different rejections into one silent boolean, so a BigTIFF becomes indistinguishable from a truncated JPEG someone renamed. PDFlibPas now separates the cases and records each one in TPDFTIFF.LastError: a header shorter than four bytes, an invalid byte-order marker, magic 43, and any other magic value all produce distinct text. The library still does not decode BigTIFF, and saying so plainly is the point. The caller gets the difference between "this is not a TIFF" and "this is a TIFF whose 64-bit offset layout the built-in decoder does not implement", which is the difference between a support ticket you can answer in one reply and one that turns into a week of guessing
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 are a different geometry, not another offsets array
PDFlibPas rejects tiled TIFF while parsing tags, before any pixel data is touched. The shortcut that invites the bug is easy to see: tag 324 (TileOffsets) and tag 325 (TileByteCounts) are arrays of file offsets and byte counts, structurally identical to the strip arrays, so pointing the existing strip fields at them costs two lines and compiles cleanly. It is also wrong. Tiles form a two-dimensional grid with padded edge blocks, their own row stride inside each tile, and no RowsPerStrip semantics whatsoever, as the tiled-image section of TIFF 6.0 spells out. Feeding tile payloads to a strip decoder therefore does not fail loudly. SimpleExtract and CompDecode walk the data with the wrong stride and emit an image with the right dimensions and the wrong pixels. The older code compounded this by keeping StripsAreTiles, ColumnsPerTile and RowsPerTile in TTIFFPage: tile geometry recorded by a decoder with no tile assembler behind it. In 3.534.1 the handlers for tags 324 and 325 raise the tile error and abandon the IFD immediately, so the refusal carries the word "tiled" instead of surfacing weeks later as a rendering complaint
One dimension clamp is not a memory budget
Clamping width and height to 65,535 each is necessary and nowhere near sufficient, because the quantity that drives allocation is a product. RowsPerStrip * Width * SamplesPerPixel can overflow 32-bit arithmetic long before either side reaches its own limit, and even without overflow it can name an allocation no service should attempt. PDFlibPas computes row bytes in Int64 and enforces three ceilings together: 65,535 per dimension, 32 colour components, and 256 MiB of decoded bytes
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// inside 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);Three details there matter more than the constants themselves. The height test is written as a division rather than a multiplication, so the oversized product is never formed at all. A RowsPerStrip below 1 or above the image height is normalised to the height first, which is the single-strip reading TIFF 6.0 already implies and which stops a hostile tag from inflating the strip buffer. And the routine is shared: ValidatePageForDecode runs at the end of tag parsing and again at the entry of both SimpleExtract and CompDecode, so code that reaches a decoder directly cannot walk around the budget. That is the same rule PDFlibPas follows when parsing untrusted PDF object graphs, because a limit enforced at one door out of three is not a limit
What must a caller check before reading PageInfo?
Check ValidTIFF first, then PageCount, and only then index PageInfo. A rejected file can leave PageCount at zero, and GetPageInfo answers an out-of-range index with an uninitialised TTIFFPage record, so an error path that reads resolution or sample counts on its way to reporting the failure ends up reading noise. Version 3.534.1 fixed both callers inside the library: the image import path reads XRes and YRes only inside the valid branch, and TPDFlib.GetImagePageCount requires ValidTIFF instead of trusting a non-zero page count on its own. Downstream, the Options argument of AddImageFromFile is the 1-based page number for a multipage TIFF, so GetImagePageCount has to be trustworthy before the loop starts rather than after it. Zero pages is now a real answer meaning "nothing here is decodable", not an accident of an early return, which matters most when you are collating and interleaving duplex scan batches and one silently mis-decoded sheet would land in the wrong position
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // bad header, BigTIFF, tiled layout or over 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;Build the decoder or link libtiff?
PDFlibPas keeps the built-in decoder, and the deciding factor is platform reach rather than authorship. Roughly 1,873 lines of Object Pascal compile anywhere the compiler goes: Win32, Win64, macOS, iOS, Android, and FPC on Linux. libtiff 4.7.1 is around 30,000 lines of C spread over 34 tif_*.c translation units, and the prebuilt object files that exist today cover Windows only. Adopting it would trade complete TIFF coverage for a supported-platform list that shrinks to whatever machine can run the C toolchain, plus a linker pass nobody has walked through yet
What that costs is worth stating without spin. The built-in decoder handles what scanned-document work actually produces: CCITT Group 3 one-dimensional and two-dimensional, Group 4, LZW, Deflate, PackBits and JPEG-in-TIFF, across WhiteIsZero, BlackIsZero, RGB, palette and CMYK photometrics with Predictor 1 and 2. Those payloads line up with the PDF filters in ISO 32000-1 §7.4.4 and §7.4.6, which is why the TIFF front-end carries so much weight in a scanning pipeline. What it does not handle is BigTIFF, tiles, floating-point Predictor 3, PixarLog and SGILog, old-style JPEG compression 6, and sub-IFD pyramids. Since 3.534.1 every one of those is a named refusal rather than a wrong image, and the library keeps a written trigger list for reopening the libtiff decision:
- a customer reports a BigTIFF file and needs native support rather than a conversion step
- a customer reports tiled TIFF from medical, GIS or industrial sources and needs it decoded in place
- a customer reports floating-point Predictor 3 TIFF
- a published vulnerability lands on the built-in CCITT or LZW decode paths
- the cross-platform argument stops applying, either because macOS, iOS and Android support is dropped, or because a reusable libtiff integration already covers macOS and Linux
The migration itself is scoped rather than hypothetical: a USE_LIBTIFF conditional would keep the TPDFTIFF public surface intact, route LoadFromStream through TIFFClientOpen with stream callbacks, and leave the Pascal parser as the non-Windows fallback. Until one of those triggers actually fires, maintaining two decoders and a doubled test matrix buys nothing a customer can feel. Deferring a cost with the escape route already written down is a different thing from ignoring it
Where this leaves a scanned-document pipeline
Treat TPDFTIFF as a gate rather than a converter. Load the file, read ValidTIFF, and log LastError verbatim whenever it is false, because that string is now the shortest route from a field report to a diagnosis. Files that fail the gate remain recoverable by converting them upstream, which is the practical answer for BigTIFF and tiled sources today. For input outside TIFF entirely, PDFlibPas takes a separate route through its AVIF, HEIF and JPEG XL image input path, so the question of which decoder owns which format stays explicit instead of emergent
All of this sits behind the ordinary image API, so a document pipeline gains the tighter boundary without changing a line of calling code beyond checking the page count it should already have been checking. If you are weighing a native TIFF-to-PDF path for Delphi or C++Builder, the full component and its image handling are documented on the PDF Library for Delphi page