PDFlibPas converts enhanced metafiles into real PDF page content record by record, rather than rasterising them, which is what keeps an imported chart or CAD drawing crisp at any zoom. That converter is about 6500 lines and it was written against the VCL, so when the library gained a Free Pascal target it was classified as unportable and stubbed out. That classification was wrong, and the way it was wrong is a useful lesson about how to audit a dependency before deciding to rewrite around it
The actual VCL surface of those 6500 lines turned out to be small: a bitmap class used for its pixel format, stream saving, handle, canvas and scanlines; a metafile class used for its width, height and handle; and the colour type with two constants. Every one of those was already provided by the library's own graphics unit, which exists precisely so the non-VCL build has equivalents. The converter was not blocked on the VCL at all. It was blocked on the Free Pascal Windows unit
Split on the axis the code actually depends on
So the change was not a reimplementation. It was one conditional: from "compile the stub when built without the VCL" to "compile the stub when not building for Windows". That is the correct axis, and stating why makes the difference obvious. An enhanced metafile is a Windows container. The converter is a parser for Windows GDI records from top to bottom. Whether the host application uses the VCL, another widget set, or no widget set at all has nothing to do with whether those records can be interpreted; whether the target is Windows has everything to do with it
The consequences of picking the right axis fall out for free. C++Builder builds, which undefine the Windows platform symbol in this library, keep the throwing stub and behave exactly as before. macOS keeps the stub, correctly, because there are no GDI records to parse there. Delphi VCL builds are untouched. And a Windows build with a non-VCL widget set gains vector EMF import as a side effect, which nobody had to implement. A conditional aligned with the real dependency turns platform work into a one-line change; a conditional aligned with the wrong one turns it into a rewrite that never gets scheduled
The Free Pascal gap was declarations, not logic
What was actually missing were the Win32 declarations that the Delphi Windows unit provides and the Free Pascal one does not. Collecting them into a single compatibility unit rather than scattering conditionals through the converter kept the parser readable. The list is instructive because it shows how uneven header coverage is between the two RTLs: 113 metafile record type constants, two extended text-output flags, three gradient fill mode constants, a handle-table pointer type, aliases for the gradient vertex and primitive records, and three record types that Free Pascal does not declare at all, covering alpha blending, transparent blitting and colour-management mode
None of that is interesting individually. All of it has to be right before the parser compiles, and a compatibility unit is the natural home because it can be diffed against the header documentation as a unit
The one that silently draws the wrong picture
Two of those declarations are not merely missing, they are present and wrong for this purpose, and this is the part worth remembering even if you never touch a metafile
Free Pascal declares the brush-creation record with the run-time brush structure embedded in it, and the extended-pen record with the run-time pen structure embedded in it. Both of those run-time structures declare their hatch member as a pointer-sized integer, because in a live GDI call that member can carry a handle. A metafile, however, always stores the 32-bit form, because the record layout is part of the serialised file format and does not change with the process bitness
On 32-bit builds the two agree and nothing happens. On Win64 the pointer-sized member is eight bytes where the file has four, so every field after the hatch member is read from the wrong offset. There is no exception, no parse error and no warning. The metafile just renders wrongly: colours from the wrong bytes, pen widths from the wrong bytes, and a picture that looks like a rendering bug rather than a struct-layout bug. Delphi ships explicitly 32-bit variants of both structures for exactly this reason, and the compatibility unit redeclares them the same way
// Wrong on Win64: Hatch is pointer-sized, the file stores 32 bits,
// and every following field shifts by four bytes with no error
type
TLogBrushRuntime = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: ULONG_PTR; // 8 bytes in a 64-bit process
end;
// Right: the serialised layout, fixed width regardless of bitness
type
TLogBrush32 = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: DWORD; // always 4 bytes, as stored in the metafile
end;
The general rule: any structure that appears both as a run-time API argument and as a serialised field layout needs two declarations, and the serialised one must use fixed-width types throughout. Pointer-sized members in a file format are always a bug waiting for a 64-bit build
Signature differences belong in a wrapper, not at every call site
The remaining differences were ordinary signature mismatches, and the way to absorb them is a forwarding wrapper rather than a conditional at each of the call sites. The transform-combining function takes pointers under Free Pascal where Delphi takes reference parameters, so the wrapper takes references and passes addresses. It also copies both source arguments into locals first, because the converter has call sites where the destination matrix is simultaneously one of the sources, and passing the same address twice to a function that writes as it reads produces a transform that is subtly wrong in a way that only shows up on rotated content
function CombineTransformCompat(var Dest: TXForm;
const A, B: TXForm): BOOL;
var
SrcA, SrcB: TXForm;
begin
// Copy first: callers legitimately pass Dest as A or B
SrcA := A;
SrcB := B;
{$IFDEF FPC}
Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;
The rectangle and point types are the other case. Free Pascal treats the metafile rectangle and point records as distinct types from the general graphics ones, so eight assignment sites needed an explicit cast between records of identical layout. Both compilers accept the cast form, so those sites carry no conditional at all, which is worth a little ugliness
What this changes for a Free Pascal deployment
Vector EMF import works on Windows under Free Pascal, producing the same page content as the Delphi build: paths as paths, gradients as pattern content, text as text. Off Windows the raster path remains the answer, and that is a limitation of the format rather than of the port. The coordinate and clipping state that the converter feeds into is described in the content stream CTM and clipping tracker article, and the vector primitives it emits are covered in vector graphics, shaders and gradients
If you are auditing your own codebase for the same opportunity, the useful exercise is the one that started this: list the members you actually use from the framework you think you depend on. The answer is often much shorter than the import list suggests, and the real constraint is usually somewhere else entirely. Device-context based import paths generally are described in the print preview and device context article, and platform and toolchain coverage is listed on the losLab PDF Developer Library product page