PDFlibPas can encode bilevel images as JBIG2 through two different backends. One is a native Object Pascal MMR encoder that is always present. The other is an external symbol-dictionary encoder that produces substantially smaller output on scanned text, and it is optional: a project has to link the backend unit for it to exist at all. That distinction is the source of the most common surprise with this feature, so it is worth stating first: DefaultJBIG2EncodeOptions requests the external encoder by default, and when the backend unit is not linked the request silently falls back to the Pascal MMR path
On Delphi and C++Builder the external backend is a set of prebuilt static objects. On Free Pascal it had to become a DLL, and the road to that conclusion is a linker story that is useful to anyone who has tried to link C++ objects into a Free Pascal program
Registration is the contract
The backend unit registers itself from its initialisation section by calling RegisterJBIG2EncoderBackend. Callers ask for it either through the options bit, PDF_JBIG2_OPTION_EXTERNAL_ENCODER, which has the value 4, or through the UseExternalEncoder parameter of the extended image entry points. The library umbrella deliberately does not pull the backend unit in, because carrying a large object set should be each project's decision; in the C++Builder tree, for instance, it is included explicitly by the projects that want it
The consequence for callers is that requesting the external encoder is a preference, not a guarantee, and a build that forgets the unit produces larger files rather than an error. If output size matters enough to ask for the better encoder, it matters enough to check that you got it
uses
PDFlibrary,
{$IFDEF FPC}
PDFlibJBIG2EncDLL; // dynamic backend for Free Pascal
{$ELSE}
PDFlibJBIG2EncC; // static object set for Delphi / C++Builder
{$ENDIF}
var
Pdf: TPDFlib;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
0, 1, 1, 0, 0, 0);
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
Compiling the unit was two lines. Symbols were the work
Getting the backend unit itself to compile under Free Pascal took exactly two changes: setting the assembler dialect, and replacing a record-based format-settings constructor with the global default variable. That is a fair reflection of how portable straightforward Pascal is between the two compilers
The symbol side was the real job. The object set references 176 C symbols. Of those, 128 already had Pascal implementations inside the unit and only needed export names attached, because Delphi uses the function name as the symbol name while Free Pascal requires an explicit public name declaration. Twenty-seven were shared with the JPEG 2000 codec and had to be exported from exactly one place, since defining them twice breaks any program that links both. The remaining 21 were platform and C runtime entries, sixteen Win32 file functions plus a handful of standard library calls, and they went into a new compatibility unit
None of that is conceptually hard, and all of it is necessary before the linker will even try. The linker is where it stopped
Three linking routes, three dead ends
The internal Free Pascal linker cannot read the object files, because they were produced by a compiler that emits associative COMDAT sections and the internal linker reports that it does not support them. That is a flat refusal, not a warning
Switching to an external linker looked like the answer. The binutils linker bundled with Free Pascal crashes outright while applying section garbage collection to this archive, and that flag is part of the fixed parameter set Free Pascal passes for the 64-bit Windows target, so it cannot be removed from the command line; the documented switches for suppressing it are ignored on this path. Supplying a much newer binutils instead fails differently: it cannot process the Free Pascal link script at all, producing an empty output without the script and a wall of relocation errors with it
A boundary discovered along the way is worth knowing even if you never hit the linker problem. The external linker resolves object file paths relative to the executable output directory rather than the source tree, so a relative include-object directive only works when the output directory happens to equal the compile-time working directory. A library cannot assume that about a consumer's project, which is on its own a reason to prefer a linked library over loose objects
Why a different C++ compiler does not help
The obvious next idea is to rebuild the C++ side with a compiler whose objects Free Pascal can read. It does not work either, and the reason is fundamental rather than a matter of switches. A minimal C++ translation unit containing a template, compiled with every code-generation feature turned off, still emits weak external symbols, because template and inline instantiation produces them by construction. Free Pascal rejects that symbol class outright. The reverse direction fails as well: a mainstream C++ linker cannot consume objects from the other compiler because of the same COMDAT section handling
So the C++ code cannot be delivered as objects to Free Pascal by any available route. It can be delivered as a DLL, which is what happened: the encoder and its image-processing dependency are built into one library exposing two flat C entry points, and the Free Pascal backend unit binds those dynamically and registers itself exactly as the static backend does. The Delphi and C++Builder path was not touched at all, which is the right outcome; a portability problem on one toolchain should not perturb the toolchain that already worked
Polarity is the one thing that will bite you
Between a Windows bilevel bitmap and a JBIG2 encoder there is a convention mismatch that no type system will catch. A one-bit-per-pixel device-independent bitmap scanline treats a set bit as white. The encoder treats a set bit as black. Hand the scanlines over unchanged and you get a perfectly valid JBIG2 stream of the photographic negative of your page
// One-bit DIB: set bit means white. JBIG2 encoder: set bit means
// black. Invert every byte on the way in
for I := 0 to RowBytes - 1 do
Row[I] := Row[I] xor $FF;
The verification method matters as much as the fix. Comparing compressed stream lengths tells you nothing, because a negative image compresses to a similar size. Looking at the page proves only that it is not obviously inverted. The reliable check is to render the output of both encoding paths, native Pascal and external, to PNG and compare them byte for byte: both encoders are lossless on the same source image, so anything other than an exact match is a bug in one of them. That comparison is now a permanent regression test, and it is the kind of assertion worth building whenever two implementations are supposed to agree exactly
Which backend to use
For general bilevel content, dithered halftones, line art, mixed graphics, the native Pascal MMR encoder is adequate and has no deployment cost. For scanned text, which is the case JBIG2 was designed for, the external symbol-dictionary encoder is where the size reduction lives, because it factors repeated glyph shapes into a dictionary instead of re-encoding every occurrence. If you are producing archives of scanned documents, that difference is large enough to change storage planning
The upstream question, how the bilevel image is produced in the first place, matters just as much for output size; region-based monochrome rendering is covered in the monochrome region rendering article, and document-wide size strategy in PDF file size optimisation and font subsetting. For scan sets with repeated pages, deduplication often beats better compression, which is the subject of perceptual image deduplication. Toolchain and backend availability per platform is listed on the losLab PDF Developer Library product page