Bringing CCITT, TIFF, PNG, Flate, and stream-buffer code up on Free Pascal is a reliable way to find Pascal that only works on Delphi by accident. None of the five patterns below involves a compiler bug. Each is code that Delphi happens to execute correctly because of an implementation detail: a hidden result parameter that aliases the caller's array, an out-of-range branch nobody ever reads past, a zero-length buffer whose only guard is a range-check switch, a 1-based offset that only one code path ever passes as 1, and a TStream.Read contract that in-memory streams never exercise. Change the compiler, or feed the same code a malformed file, and the accident stops holding
What follows is the specific shape of each trap, the way PDF Library for Delphi handles it, and the discipline behind it: the same source has to produce the same document semantics on both compilers, and the only way to know is to check on both. The sibling article on hardening a Pascal PDF parser against malicious files covers integer width, recursion depth, and uninitialized buffers. This one is about a different failure class: code that is wrong all along while a compiler quietly covers for it
Why does a function returning a dynamic array work without SetLength on Delphi?
Because Delphi passes the caller's own variable in as the hidden result parameter, so a function that never allocates its result can still write into an array the caller allocated. TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray is the reference-line lookup at the heart of two-dimensional Group 3 and Group 4 decoding: given the current position a0 and the color of the current run, it searches the previous scanline's changing elements, the b1 and b2 of the ITU-T T.4 and T.6 two-dimensional coding scheme, and returns them as a two-slot array. The tempting implementation writes Result[0] and Result[1] and never calls SetLength on Result at all
That should fault on the first write, and on Free Pascal it does. On Delphi it does not, as long as the call sites look like this: declare b: TCCITTIntegerArray, run SetLength(b, 2) once before the scanline loop, then inside the loop assign b := GetNextChangingElement(a0, IsWhite) and read b[0] and b[1]. The Delphi language guide states that a function whose result is a long string, dynamic array, or other managed type receives that result as an additional var parameter, and in practice the compiler passes the address of the assignment target. So Result inside the function is b itself, already two elements long, and every write lands in memory the caller owns. Free Pascal hands the function a fresh nil array and assigns it to b afterward, which is the reading of the contract the code should be written against in the first place
The aliasing also carries a semantic the decoder depends on. Result[0] is only assigned when the scan finds an element greater than a0, and Result[1] only when there is an element after it, so on a miss the slots keep whatever the previous iteration left in b. The obvious fix, allocate two slots and zero them on every call, destroys that carry-over and changes decoded output on Delphi. PDF Library for Delphi uses a guard instead of a reset: on Delphi it is dead code and the decode path stays byte for byte the same, and on Free Pascal it turns a fault into the intended behavior. That asymmetry is the whole point, since the guard has to be a no-op on the compiler where the code already produces verified output
Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
IsWhite: Boolean): TCCITTIntegerArray;
Begin
// Delphi arrives here with the caller's two-element array aliased
// as Result, so this is a no-op there. FPC arrives with nil.
If (Length(Result) < 2) Then
SetLength(Result, 2);
...
// Result[0] / Result[1] are still written only on a hit, so a miss
// keeps the previous iteration's values exactly as before
End;
A count that outlived its data: the TIFF directory entry
When you invalidate an array, you have to invalidate its count in the same statement, or the count will be believed by code that never sees the array. A TIFF image file directory entry (TIFF 6.0 §2, the 12-byte layout of tag, type, count, and value-or-offset) carries a 32-bit count straight out of the file, and PDF Library for Delphi reads each one through PopDE: TTIFFEntry, a record with Tag, TagType, Length, Offset, and the decoded IntegerValues and DoubleValues arrays. The naive range check tests whether Offset + TypeSize * Length runs past the end of the file and, if it does, sets both arrays to zero length while leaving Result.Length at the value from the file
Two things go wrong from there. The function ends with a fallback that reads "if Length is zero, give the entry one zero-valued element" so that callers can always read element zero. With Length never cleared on the out-of-range path, that fallback never fires for the one case it exists for. And the callers do read element zero, unconditionally: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip, and a dozen more take E.IntegerValues[0], and the strip tables do Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4), copying Length times four bytes out of an array that has none. A cleared array with a live count is strictly more dangerous than an unchecked one, because the unchecked one at least holds the bytes it claims to
The second trap is ordering. Run the two SetLength calls before the range test, sized from the file's count, and a hostile entry can request a multi-gigabyte allocation before a single validity check. On Delphi the resulting exception is typically caught by a handler further up the image-loading path and the file simply fails to load, which is why the problem is easy to miss; what actually happens is an out-of-memory event the file chose. The correct order allocates after the test and makes the count travel with the data
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
> Length(Source);
If OutOfRange Then
Begin
Result.Length := 0; // the count goes with the values
SetLength(Result.IntegerValues, 0);
SetLength(Result.DoubleValues, 0);
End
Else
Begin
SetLength(Result.IntegerValues, Result.Length); // only now
SetLength(Result.DoubleValues, Result.Length);
End;
// ... later, the fallback reaches the case it exists for:
If (Result.Length = 0) Then
Begin
SetLength(Result.IntegerValues, 1);
Result.IntegerValues[0] := 0;
End;
Nothing about this one is compiler-specific, which is what makes it belong in this list. The trap stays latent on Delphi for the same reason it stays latent on Free Pascal: it takes a directory entry pointing past the end of the file, and ordinary test files do not have one. A port does not expose it. Reading the code with the question "what does Delphi do for me here that I am not doing myself" does
What happens when a PNG IHDR claims a color type the format does not define?
PDF Library for Delphi rejects the image before the row filters run. ISO 15948 §11.2.2 defines the IHDR chunk and Table 11.1 lists the six legal color type and bit depth combinations: grayscale at 1, 2, 4, 8, or 16 bits, indexed color at 1, 2, 4, or 8, and truecolor, grayscale with alpha, and truecolor with alpha at 8 or 16. The trap is a reader that validates the compression method and the filter method fields of IHDR and passes FColorType and the bit depth through untouched: an illegal color type then yields a zero-byte scanline, and the unfilter loops get an empty buffer
Row filter code typically sizes everything from a Case FColorType Of that maps each color type to a component count. A color type outside the six falls into the Else branch, where SourceComponents is 0, so ScanlineByteCount is 0, so SetLength(PreviousScanline, 0) is followed immediately by FillChar(PreviousScanline[0], ScanlineByteCount, 0). Indexing element zero of an empty dynamic array is an address computed from nil. With range checking off, a zero-byte fill through that address is a silent no-op and the decoder marches on through rows that do not exist; with range checking on it is an ERangeError on the first image; and the Move calls that follow it are a step away from an access violation. Which of those you get depends on the compiler and on build switches rather than on anything the decoder decided, and that is the tell that the decoder never decided at all
PDF Library for Delphi applies the table from the specification where the other IHDR fields are checked: COLOR_GRAYSCALE accepts FSourceBitDepth in [1, 2, 4, 8, 16], COLOR_PALETTE accepts [1, 2, 4, 8], and COLOR_RGB, COLOR_GRAYSCALEALPHA, and COLOR_RGBALPHA accept [8, 16]; anything else clears ValidImage and the image is refused with its width and height intact for diagnostics. A pHYs chunk shorter than its nine bytes is checked in the same place, so the DPI reader never indexes S[1] through S[8] of a string the short chunk left empty
A 1-based offset treated as a 0-based pointer
InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString takes a 1-based StartPos, because its input is an AnsiString and the Delphi implementation addresses the zlib input as @Input[StartPos]. The Free Pascal implementation, written against paszlib so both Windows targets link compression statically, has to address the same input through pointer arithmetic, and pointer arithmetic is 0-based. Set next_in to PAnsiChar(Input) + StartPos and avail_in to Length(Input) - StartPos, pass 1, which is what "start at the beginning" means for this function, and the FPC build starts inflating at the second byte and stops one byte before the end
This one hides easily because the caller most tests reach is InflateStr, which passes 0. Zero happens to be the correct 0-based offset, so the two builds agree on every plain InflateStr call and every test that goes through it. TPDFDocument.DecodeAllStreams, the routine SaveQDFToFile and ConvertFileToQDF use to expand single-FlateDecode streams into readable form, passes 1. With the off-by-one in place, the skipped zlib header makes the inflate fail, but the zlib stream still reports a nonzero Consumed for the bytes it examined, so a caller that trusts Consumed takes the empty payload as a successful decode and replaces every content stream with an empty string. The resulting QDF has the right page count, valid structure, and no page content, which is a file that opens without an error in every viewer and shows nothing
// FPC branch of InflateStrFromPosition.
// StartPos is 1-based like the Delphi branch; clamp it, then convert
// to a 0-based pointer offset exactly once, at the boundary.
If (StartPos < 1) Then
StartPos := 1;
If (Length(Input) = 0) Or (StartPos > Length(Input)) Then
Exit;
...
strm.next_in := Pointer(PAnsiChar(Input) + StartPos - 1);
strm.avail_in := Length(Input) - StartPos + 1;
The check that catches it is the smallest possible one: deflate a payload, inflate it from position 0 and from position 1, and confirm both return the same payload and both report Consumed equal to the full stream length. An RFC 1950 stream has a two-byte header and a four-byte Adler-32 trailer, so an off-by-one at either end is not a subtle corruption, it is a stream that either fails to start or fails to finish. The lesson is about the boundary, not zlib: when a function's parameter is defined in one index base and the implementation underneath uses the other, the conversion belongs in exactly one line, and a test has to call it with the value that distinguishes the two bases
Why is a short TStream.Read not the end of the stream?
Because TStream.Read is allowed to return fewer bytes than requested for any reason it likes, and only a return of 0 means there is nothing more. TMemoryStream and TFileStream on a local disk almost always fill the request, which is why code that treats "returned less than I asked for" as end-of-file passes every test that uses them. Network-backed streams, decompression streams, and any TStream descendant a customer wrote can return two bytes when asked for sixty-four thousand and still have gigabytes behind them
TPLBuffer is the reader every parser in PDF Library for Delphi goes through, and it can wrap an AnsiString, a pointer, a byte array, or a TStream. Its four scanning queries, DistanceToByte, DistanceToOtherByte, DistanceToAnyByte, and DistanceToOtherBytes, all returning Int64, read the source in 64 KB blocks looking for a delimiter and report how far away it is without moving the logical position. The natural way to end such a loop is Until ReadCount < BlockSize. For the three in-memory sources that is correct, since ReadIntoBuffer always delivers the full block until the last one. For a stream source it means the scan gives up on the first short read, reports the delimiter as absent, and the tokenizer above it decides the object ends where it does not, which is why TPLBuffer loops until a read returns 0
// TPLBuffer.DistanceToByte scan loop.
// Zero is the only end-of-data signal TStream.Read defines.
TempPosition := FPosition;
Try
Repeat
ReadCount := ReadIntoBuffer(@TempBuffer[0], BlockSize);
For TestPos := 0 To ReadCount - 1 Do
If TempBuffer[TestPos] = Value Then
Begin
Result := TotalSkipped + TestPos;
Exit;
End;
Inc(TotalSkipped, ReadCount);
Until ReadCount = 0;
Finally
FPosition := TempPosition; // a peek must not move the reader
End;
The way to pin this down is a TMemoryStream descendant whose Read override caps every request at two bytes. Wrap the string aaaaaX in it, set the buffer position to 1, and all four queries must report a distance of 4 to the X, leave the position at 1 afterward, and report -1 for a byte that is not there. A scan that stops on the first short read sees two bytes, concludes the stream is exhausted, and returns -1. The finally matters as much as the loop condition: an Exit from inside the scan is the normal success path, and the logical position has to be restored on that path too, not only when the loop runs to completion
One source, two compilers, one set of assertions
The discipline behind these five is that "the Delphi build passes" is evidence about Delphi, not about the source. A cross-compiler check worth having runs one routine under both compilers: build a two-page document with compressed content through TPDFlib, save it, save it again as QDF through SaveQDFToFile, repair the QDF with RepairQDFFile, encrypt the plain file with AES-128 through EncryptFile and a permission mask from EncodePermissions, and then reload every artifact and check the same things on both compilers: page count is 2, the title survives, page two's text extracts intact from the plain, repaired, and encrypted files, the wrong password is refused with a nonzero LastErrorCode, EncryptionStrength is 128, EncryptionAlgorithm is 2, and the individual permission bits from GetUserPermissions come back exactly as encoded
Compare normalized semantics rather than bytes. Encryption draws random salts and the writer assigns document identifiers, so the two builds are not expected to emit identical files; they are expected to emit files that mean the same thing, and the checks should be phrased at that level. The QDF leg earns its place because of the offset trap: a QDF with two pages and no content passes a page-count check and fails a text-extraction check, so check the second one. Any change that is a no-op on one compiler and a behavior change on the other, which describes four of the five above, has to clear the same checks on both
The link-time half of the same port, getting Delphi's OMF objects and Free Pascal's COFF expectations to agree, is its own story in FPC Win32 OMF to COFF object linking, and the structural hardening of the same TIFF reader against BigTIFF and tiled files is in the built-in TIFF decoder notes. The decoders in this article ship in the PDF Library for Delphi for Delphi, C++Builder, and Free Pascal, where the same source is expected to earn the same result on every compiler it targets rather than be granted it by one