PDFlibPas превращает расширенные метафайлы в настоящее содержимое страниц PDF запись за записью, а не растрирует их — именно поэтому импортированная диаграмма или чертёж CAD остаются чёткими при любом масштабе. Этот конвертер насчитывает около 6500 строк и был написан поверх VCL, поэтому когда у библиотеки появилась цель на Free Pascal, его классифицировали как непереносимый и заменили заглушкой. Классификация оказалась неверной, и сам характер этой ошибки — полезный урок о том, как проверять зависимость, прежде чем решаться на переписывание вокруг неё
Реальная опора на VCL в этих 6500 строках оказалась небольшой: класс растрового изображения, использовавшийся ради пиксельного формата, сохранения в поток, дескриптора, канвы и строк развёртки; класс метафайла, от которого нужны ширина, высота и дескриптор; и тип цвета с двумя константами. Всё это уже имелось в собственном графическом модуле библиотеки, который существует именно для того, чтобы сборка без VCL имела эквиваленты. Конвертер вовсе не упирался в VCL. Он упирался в модуль Windows из Free Pascal
Делите по той оси, от которой код зависит на самом деле
Поэтому изменение не было повторной реализацией. Это была одна директива условной компиляции: с «компилировать заглушку при сборке без VCL» на «компилировать заглушку, когда сборка ведётся не под Windows». Именно эта ось верна, и объяснение почему делает разницу очевидной. Расширенный метафайл — это контейнер Windows. Конвертер сверху донизу является разборщиком записей Windows GDI. То, использует ли основное приложение VCL, другой набор виджетов или не использует виджетов вовсе, никак не связано с тем, можно ли интерпретировать эти записи; то, является ли целевой платформой Windows, связано со всем
Последствия правильного выбора оси достаются бесплатно. Сборки C++Builder, в которых в этой библиотеке снимается определение символа платформы Windows, сохраняют заглушку с исключением и ведут себя ровно как раньше. macOS сохраняет заглушку, и правильно, ведь разбирать там записи GDI нечего. Сборки Delphi с VCL не затронуты. А сборка под Windows с набором виджетов без VCL в качестве побочного эффекта получает импорт векторных EMF, который никому не пришлось реализовывать. Условная компиляция, выровненная по реальной зависимости, превращает платформенную работу в изменение одной строки; выровненная по неверной — в переписывание, которое никогда не попадёт в план
Пробел Free Pascal был в объявлениях, а не в логике
На деле отсутствовали объявления Win32, которые предоставляет модуль Windows в Delphi и не предоставляет модуль Free Pascal. Сбор их в единый модуль совместимости, вместо того чтобы раскидывать условные директивы по конвертеру, оставил разборщик читаемым. Список показателен: он демонстрирует, насколько неравномерно покрытие заголовков в двух RTL — 113 констант типов записей метафайла, два флага расширенного текстового вывода, три константы режимов градиентной заливки, тип указателя таблицы дескрипторов, псевдонимы для записей вершин и примитивов градиента и три типа записей, которые Free Pascal не объявляет вовсе: альфа-смешивание, прозрачное блиттирование и режим управления цветом
По отдельности ничего из этого не интересно. Всё это должно быть верным, прежде чем разборщик скомпилируется, а модуль совместимости — естественное место, поскольку его можно сверять с документацией заголовков как единое целое
Тот случай, когда картинка молча рисуется неверно
Два из этих объявлений не просто отсутствуют — они присутствуют, но для этой цели неверны, и именно эту часть стоит запомнить, даже если вы никогда не коснётесь метафайла
Free Pascal объявляет запись создания кисти со встроенной структурой кисти времени выполнения, а запись расширенного пера — со встроенной структурой пера времени выполнения. Обе структуры времени выполнения объявляют член hatch как целое размером с указатель, потому что в живом вызове GDI этот член может нести дескриптор. Метафайл же всегда хранит 32-битную форму, поскольку раскладка записи — часть сериализованного формата файла и не меняется вместе с разрядностью процесса
На 32-битных сборках оба варианта совпадают, и ничего не происходит. На Win64 член размером с указатель занимает восемь байт там, где в файле четыре, поэтому каждое поле после члена hatch читается с неверного смещения. Никакого исключения, никакой ошибки разбора и никакого предупреждения нет. Метафайл просто отрисовывается неверно: цвета из чужих байтов, толщины перьев из чужих байтов, и картина, похожая на баг отрисовки, а не на баг раскладки структуры. Delphi именно по этой причине поставляет явно 32-битные варианты обеих структур, и модуль совместимости переобъявляет их таким же образом
// Неверно на Win64: Hatch имеет размер указателя, файл хранит 32 бита,
// и каждое последующее поле сдвигается на четыре байта без ошибки
type
TLogBrushRuntime = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: ULONG_PTR; // 8 байт в 64-битном процессе
end;
// Верно: сериализованная раскладка фиксированной ширины независимо от разрядности
type
TLogBrush32 = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: DWORD; // всегда 4 байта, как хранится в метафайле
end;
Общее правило: любая структура, выступающая и как аргумент API времени выполнения, и как сериализованная раскладка полей, нуждается в двух объявлениях, а сериализованное должно сплошь использовать типы фиксированной ширины. Члены размером с указатель в файловом формате — это всегда баг, ожидающий 64-битной сборки
Различия сигнатур живут в обёртке, а не в каждой точке вызова
Оставшиеся различия были обычными несовпадениями сигнатур, и способ их поглотить — перенаправляющая обёртка, а не условная директива в каждой точке вызова. Функция комбинирования трансформаций под Free Pascal принимает указатели там, где Delphi принимает параметры-ссылки, поэтому обёртка принимает ссылки и передаёт адреса. Она также сначала копирует оба исходных аргумента в локальные переменные: у конвертера есть точки вызова, где матрица-приёмник одновременно является одним из источников, а передача одного и того же адреса дважды функции, которая пишет по мере чтения, даёт трансформацию, тонко неверную так, что это проявляется только на повёрнутом содержимом
function CombineTransformCompat(var Dest: TXForm;
const A, B: TXForm): BOOL;
var
SrcA, SrcB: TXForm;
begin
// Сначала копия: вызывающие по праву передают Dest как A или B
SrcA := A;
SrcB := B;
{$IFDEF FPC}
Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;
Типы прямоугольника и точки — другой случай. Free Pascal считает записи прямоугольника и точки метафайла отличными типами от общих графических, поэтому восемь мест присваивания потребовали явного приведения между записями одинаковой раскладки. Оба компилятора принимают форму с приведением, так что на этих местах вообще нет условных директив — ради этого стоит немного некрасивости
Что это меняет для развёртывания на Free Pascal
Импорт векторных EMF работает на Windows под Free Pascal, давая то же содержимое страниц, что и сборка Delphi: пути как пути, градиенты как содержимое шаблонов, текст как текст. Вне Windows остаётся ответом растровый путь, и это ограничение формата, а не переноса. Координатное состояние и состояние отсечения, которые питает конвертер, описаны в статье о трекере CTM и отсечения потока содержимого, а выдаваемые им векторные примитивы рассмотрены в материале о векторной графике, шейдерах и градиентах
Если вы проверяете собственную кодовую базу на ту же возможность, полезное упражнение — то, с которого всё началось: перечислите члены, которые вы действительно используете из каркаса, от которого, как вам кажется, вы зависите. Ответ часто оказывается куда короче, чем подсказывает список импорта, а настоящее ограничение обычно пребывает совсем в другом месте. Пути импорта на основе контекста устройства в целом описаны в статье о предварительном просмотре печати и контексте устройства, а покрытие платформ и инструментальных цепочек перечислено на странице продукта losLab PDF Developer Library