HotPDF compiles and runs under Free Pascal 3.2.2 with Lazarus, and the honest summary of that port is two sentences. Document creation, loading, saving, compression, decompression, encryption and decryption all work on Pascal-only backends, so a Lazarus application can produce and consume real PDF without any C dependency. The optional native image codecs do not, because the prebuilt Win64 objects use a COFF flavour that neither Free Pascal linker can consume, so on that toolchain the entry points resolve to stubs that fail closed
Getting from "compiles" to "works" took a specific set of fixes, and every one of them is a trap that will find any other Delphi codebase moving to Free Pascal. They are worth writing down in the order they hurt
Why does a unit compiling prove nothing?
Because a Pascal unit can reference a symbol that will never do anything useful and still satisfy the compiler. At the point all 113 library units built cleanly under Free Pascal, the archive container handlers genuinely worked, verified by a smoke test that opened a CBZ and converted it to PDF. XFA form flattening did not work at all, because flattening has to inflate the compressed /XFA packet stream and the deflate entry point was still a stub. Nothing in the build output distinguished those two cases
The rule that came out of it is short. Before writing in a release note that a feature works on a new toolchain, write a runtime probe that exercises the feature end to end on that toolchain. Compile coverage is a prerequisite, never evidence. The broader picture of what the port covers is in the Free Pascal and Lazarus Win64 support notes
A raise inside a cdecl stub does not reach the caller
This one deserves its own section because the symptom is so misleading. The stub units expose C entry points the way a static library would, so a stub looks like this
// Looks reasonable. Is not.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
public name 'inflate';
begin
raise ENotSupportedException.Create('codec unavailable');
end;
On Free Pascal for Win64 that exception does not propagate to the caller. There is no try..except handler that sees it, because unwinding across a cdecl boundary declared this way does not carry the Pascal exception frame; the process terminates with exit code 217. From the application side there is no error, no message and no log line, just a program that vanishes. That is strictly worse than a wrong answer, because a wrong answer can be handled
The tempting fix is to make the stub return a failure code instead, and for inflate that is right because zlib has a well-defined error return. It is wrong in general: a stub for jpeg_read_header that returns zero tells the caller to carry on with a structure nobody initialised. The durable fix is to gate at the Pascal entry point rather than inside the C-shaped stub, using whatever failure convention that API already has
function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
// Refuse before the stub is ever reached, with this API's own
// failure convention rather than an exception across cdecl
Bitmap := nil;
Result := False;
Exit;
{$ENDIF}
Result := DecodeJPEGNative(Data, Bitmap);
end;
paszlib is not zlib, and the difference is two document classes
The Pascal deflate implementation available on Free Pascal handles two framings: the zlib wrapper and raw deflate. It does not handle gzip framing, which zlib selects through windowBits values from 16 to 31, and it does not handle the automatic detection mode that values 32 to 47 select. HotPDF needs both. The safe SVG import path asks for 31, and the loader has a fallback ladder that asks for 47 when a stream's framing is ambiguous. Skip either one and an entire family of documents stops opening, with a decode error that points at the stream rather than at the missing framing
There is a second, sharper incompatibility. The z_stream record that paszlib declares does not have the same memory layout as the C one: its msg field is a short string rather than a pointer, and total_in and total_out are 64-bit where the C ABI has machine words. A caller record therefore cannot be passed straight through. The working arrangement is to keep the paszlib state behind the state pointer that the public record already reserves, and to copy the public fields in and out around every call. The gzip CRC and the eight-byte length trailer are accounted for in that same shim layer, which is the natural place for them since it already owns the framing decision
Passing a dynamic array to an untyped var parameter
This is the bug most likely to be sitting in your code right now. When you pass a dynamic array to an untyped var parameter, what the callee receives is the address of the array variable, which is the address of a pointer, not the address of the payload. So a read into it overwrites the variable itself and whatever sits next to it
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// Wrong: hands over the address of the FBuffer variable
FStream.Read(FBuffer, Length(FBuffer));
// Right: hands over the address of the first payload byte
FStream.Read(FBuffer[0], Length(FBuffer));
end;
On Delphi the wrong form frequently appears to work, because what it corrupts is an adjacent stack slot that nothing reads afterwards. On Free Pascal the same line segmentation-faults on first use. What makes it so hard to spot by eye is that static arrays have no such problem, since a static array variable is its own payload, so both spellings are correct in the same file depending on the declaration a few hundred lines away
ZIP containers without System.Zip
Free Pascal has no equivalent of the RTL zip unit, and the available alternative has both a different API surface and no support for the legacy encryption that older container formats still use, so a small in-library reader turned out shorter than adapting to it. Two format details cost time and are easy to get wrong
The first is the encryption header check byte. Its twelfth byte is normally the high byte of the CRC, but when general-purpose flag bit 3 is set, meaning the sizes live in a trailing data descriptor and the CRC is not yet known, the check byte comes from the high byte of the modification time instead. Implement only the CRC form and every archive written in streaming mode rejects a correct password. The second is the ZIP64 extra field: its three 64-bit fields appear in a fixed order but are only written when the corresponding 32-bit field is saturated, so reading them at fixed offsets works on the archives you tested and fails on the next one. Parse them positionally against which 32-bit fields are saturated
One convenience worth knowing: the Free Pascal decompression stream takes a second constructor argument that skips the zlib header, which is exactly what ZIP entries need since they store raw deflate. That path does not touch the library zlib shim at all, so it is unaffected by the missing C backend
Colour glyph transparency under the LCL
Reading the alpha channel of a rasterised colour glyph is the one graphics detail with no direct translation. The LCL PNG class has no scanline accessor that exposes alpha, and assigning a PNG to a bitmap discards it, so a colour emoji arrives fully opaque and composites with a black box behind it. The working route is the interface image: create it from the PNG, then read pixels through the colour accessor, remembering that its components are 16-bit and need shifting down by eight to become bytes. That surface also uses natural top-down row order, so the Height - 1 - Y inversion that VCL scanline code needs must be removed rather than ported
Two build-system notes before you file a bug
A full rebuild occasionally fails with an undefined symbol whose name ends in a $crc suffix and a hex value. That suffix is computed from the parameter types, and it fails to match when one build compiles a unit against two different interface versions in the same pass. Rerunning the build clears it; the signature is not wrong
Second, Free Pascal 3.2.2 has no anonymous methods, so anywhere the library used closures to wire up a parallel pipeline the Free Pascal build takes a deterministic serial fallback instead. Output is identical, throughput is not; if you depend on parallel page rendering, that is a reason to stay on Delphi for the moment, and the pipeline design is described in the parallel render pipeline article. The image codec situation is the other place where toolchain choice changes capability rather than only speed, so a Lazarus deployment should plan its image formats accordingly; the current per-toolchain matrix is on the HotPDF Delphi PDF component product page