HotPDF компилируется и работает под Free Pascal 3.2.2 с Lazarus, и честное резюме этого переноса умещается в два предложения. Создание, загрузка, сохранение документов, сжатие, распаковка, шифрование и расшифровка работают на чисто Pascal-бэкендах, поэтому приложение Lazarus может производить и потреблять настоящий PDF без какой-либо зависимости от C. Необязательные нативные кодеки изображений — нет: заранее собранные объекты Win64 используют разновидность COFF, которую не может потребить ни один компоновщик Free Pascal, поэтому на этой инструментальной цепочке точки входа превращаются в заглушки, отказывающие закрыто
Путь от «компилируется» до «работает» потребовал конкретного набора исправлений, и каждое из них — ловушка, которая найдёт любую другую кодовую базу Delphi, переезжающую на Free Pascal. Их стоит записать в том порядке, в каком они болели
Почему компиляция модуля ничего не доказывает?
Потому что модуль Pascal может ссылаться на символ, который никогда не сделает ничего полезного, и всё же удовлетворить компилятор. К моменту, когда все 113 модулей библиотеки чисто собрались под Free Pascal, обработчики архивных контейнеров по-настоящему работали, что подтвердил дымовой тест, открывший CBZ и сконвертировавший его в PDF. Спрямление форм XFA не работало вовсе: спрямление должно распаковывать сжатый поток пакетов /XFA, а точка входа deflate всё ещё была заглушкой. Ничто в выводе сборки не различало эти два случая
Правило, которое из этого вышло, коротко. Прежде чем писать в примечании к выпуску, что возможность работает на новой инструментальной цепочке, напишите проверку времени выполнения, прогоняющую эту возможность от начала до конца на этой цепочке. Компиляционное покрытие — предпосылка, но никогда не свидетельство. Более широкая картина того, что покрывает перенос, — в заметках о поддержке Free Pascal и Lazarus Win64
Исключение внутри cdecl-заглушки не доходит до вызывающего
Этот случай заслуживает собственного раздела, потому что симптом так вводит в заблуждение. Модули заглушек открывают точки входа C так, как это делала бы статическая библиотека, поэтому заглушка выглядит так
// Выглядит разумно. Но нет.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
public name 'inflate';
begin
raise ENotSupportedException.Create('codec unavailable');
end;
Под Free Pascal для Win64 это исключение не доходит до вызывающего. Ни один обработчик try..except его не увидит: раскрутка через границу cdecl, объявленную так, не несёт кадра исключений Pascal; процесс завершается с кодом выхода 217. Со стороны приложения нет ни ошибки, ни сообщения, ни строки журнала — просто программа исчезает. Это строго хуже неверного ответа, ведь неверный ответ можно обработать
Соблазнительное исправление — заставить заглушку возвращать код отказа, и для inflate это верно, ведь zlib имеет чётко определённый возврат ошибки. В общем случае это неверно: заглушка для jpeg_read_header, возвращающая ноль, велит вызывающему продолжать со структурой, которую никто не инициализировал. Прочное исправление — ставить заслон на точке входа Pascal, а не внутри C-образной заглушки, используя ту конвенцию отказа, которая у этого API уже есть
function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
// Отказ до того, как будет достигнута заглушка, собственной
// конвенцией отказа этого API, а не исключением через cdecl
Bitmap := nil;
Result := False;
Exit;
{$ENDIF}
Result := DecodeJPEGNative(Data, Bitmap);
end;
paszlib — не zlib, и разница стоит двух классов документов
Доступная на Free Pascal реализация deflate на Pascal обрабатывает две обрамляющие схемы: обёртку zlib и сырой deflate. Она не обрабатывает обрамление gzip, которое zlib выбирает значениями windowBits от 16 до 31, и не обрабатывает режим автоматического обнаружения, который выбирают значения от 32 до 47. HotPDF нужны оба. Безопасный путь импорта SVG запрашивает 31, а у загрузчика есть лестница отката, запрашивающая 47, когда обрамление потока неоднозначно. Пропустите любое из двух — и целое семейство документов перестанет открываться с ошибкой декодирования, указывающей на поток, а не на отсутствующее обрамление
Есть и вторая, более острая несовместимость. Запись z_stream, которую объявляет paszlib, не совпадает по раскладке памяти с C-версией: её поле msg — короткая строка, а не указатель, а total_in и total_out 64-битные там, где в C ABI машинные слова. Поэтому запись вызывающего нельзя передать напрямую. Рабочая схема — держать состояние paszlib за указателем state, который публичная запись уже резервирует, и копировать публичные поля туда и обратно вокруг каждого вызова. CRC gzip и восьмибайтовый хвост длины учитываются в той же прослойке, и это естественное место для них, поскольку именно она уже принимает решение об обрамлении
Передача динамического массива в нетипизированный var-параметр
Это баг, который вероятнее всего прямо сейчас сидит в вашем коде. Когда вы передаёте динамический массив в нетипизированный параметр var, вызываемый получает адрес переменной массива, то есть адрес указателя, а не адрес полезной нагрузки. Поэтому чтение в него перезаписывает саму переменную и то, что лежит рядом
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// Неверно: передаётся адрес переменной FBuffer
FStream.Read(FBuffer, Length(FBuffer));
// Верно: передаётся адрес первого байта полезной нагрузки
FStream.Read(FBuffer[0], Length(FBuffer));
end;
В Delphi неверная форма часто кажется работающей: портится соседняя ячейка стека, которую потом никто не читает. В Free Pascal та же строка даёт segmentation fault при первом же использовании. Глазами это так трудно заметить потому, что у статических массивов такой проблемы нет: переменная статического массива сама является своей полезной нагрузкой, поэтому оба написания корректны в одном файле в зависимости от объявления за несколько сотен строк
ZIP-контейнеры без System.Zip
В Free Pascal нет эквивалента zip-модуля RTL, а доступная альтернатива имеет и другую поверхность API, и отсутствие поддержки устаревшего шифрования, которое старые форматы контейнеров всё ещё используют, поэтому небольшой собственный модуль чтения внутри библиотеки оказался короче, чем адаптация к ней. Две детали формата стоили времени и легко делаются неверно
Первая — контрольный байт заголовка шифрования. Его двенадцатый байт — обычно старший байт CRC, но когда установлен бит 3 общего флага, то есть размеры лежат в завершающем дескрипторе данных, а CRC ещё неизвестен, контрольный байт берётся вместо этого из старшего байта времени изменения. Реализуете только CRC-форму — и каждый архив, записанный в потоковом режиме, отвергнет правильный пароль. Вторая — дополнительное поле ZIP64: его три 64-битных поля идут в фиксированном порядке, но записываются только когда соответствующее 32-битное поле насыщено, поэтому чтение по фиксированным смещениям работает на протестированных архивах и ломается на следующем. Разбирайте их позиционно в зависимости от того, какие 32-битные поля насыщены
Одно удобство, о котором стоит знать: поток распаковки Free Pascal принимает второй аргумент конструктора, пропускающий заголовок zlib, — именно то, что нужно записям ZIP, ведь они хранят сырой deflate. Этот путь вообще не касается zlib-прослойки библиотеки, поэтому отсутствие C-бэкенда на него не влияет
Прозрачность цветных глифов под LCL
Чтение альфа-канала растрированного цветного глифа — единственная графическая деталь без прямого перевода. Класс PNG в LCL не имеет доступа к строкам развёртки, открывающего альфу, а присваивание PNG растровому изображению её отбрасывает, поэтому цветной эмодзи приходит полностью непрозрачным и компонуется с чёрным ящиком позади. Рабочий маршрут — интерфейсное изображение: создать его из PNG, затем читать пиксели через цветовой аксессор, помня, что его компоненты 16-битные и требуют сдвига вниз на восемь, чтобы стать байтами. Эта поверхность использует и естественный порядок строк сверху вниз, поэтому инверсия Height - 1 - Y, нужная коду строк развёртки VCL, должна быть убрана, а не перенесена
Две заметки о системе сборки, прежде чем отправлять отчёт об ошибке
Полная пересборка изредка падает с неопределённым символом, чьё имя кончается суффиксом $crc и шестнадцатеричным значением. Суффикс вычисляется из типов параметров и не совпадает, когда одна сборка компилирует модуль против двух разных версий интерфейса в одном проходе. Повторный запуск сборки снимает проблему; подпись не неверна
Во-вторых, у Free Pascal 3.2.2 нет анонимных методов, поэтому всюду, где библиотека использовала замыкания для сборки параллельного конвейера, сборка Free Pascal берёт детерминированный последовательный резерв. Результат идентичен, пропускная способность — нет; если вы зависите от параллельного рендеринга страниц, это повод пока оставаться на Delphi, а дизайн конвейера описан в статье о параллельном конвейере рендеринга. Ситуация с кодеками изображений — другое место, где выбор цепочки меняет возможности, а не только скорость, поэтому развёртыванию Lazarus стоит заранее спланировать форматы изображений; текущая матрица по цепочкам — на странице продукта HotPDF Delphi PDF component