PDFlibPas может кодировать двухуровневые изображения в JBIG2 через два разных бэкенда. Один — собственный MMR-кодировщик на Object Pascal, который присутствует всегда. Другой — внешний кодировщик словарей символов, дающий на отсканированном тексте существенно меньший результат, и он необязателен: проект должен подключить модуль бэкенда, чтобы тот вообще существовал. Это различие — источник самого частого сюрприза с данной возможностью, поэтому скажем о нём сразу: DefaultJBIG2EncodeOptions запрашивает внешний кодировщик по умолчанию, и когда модуль бэкенда не подключён, запрос молча откатывается на Pascal-путь MMR
В Delphi и C++Builder внешний бэкенд — это набор заранее собранных статических объектных файлов. В Free Pascal ему пришлось стать DLL, и путь к этому выводу — история о компоновщике, полезная всякому, кто пытался прилинковать объекты C++ к программе на Free Pascal
Регистрация — это контракт
Модуль бэкенда регистрирует себя из секции инициализации вызовом RegisterJBIG2EncoderBackend. Обращаются к нему либо через бит опций PDF_JBIG2_OPTION_EXTERNAL_ENCODER со значением 4, либо через параметр UseExternalEncoder расширенных точек входа для изображений. Зонтичный модуль библиотеки намеренно не притягивает модуль бэкенда: носить с собой большой набор объектных файлов должно быть решением каждого проекта; в дереве C++Builder, например, его подключают явно те проекты, которым он нужен
Следствие для вызывающих: запрос внешнего кодировщика — это предпочтение, а не гарантия, и сборка, забывшая модуль, даёт файлы большего размера, а не ошибку. Если размер результата важен настолько, что вы просите лучший кодировщик, он важен настолько, чтобы проверить, что вы его получили
uses
PDFlibrary,
{$IFDEF FPC}
PDFlibJBIG2EncDLL; // динамический бэкенд для Free Pascal
{$ELSE}
PDFlibJBIG2EncC; // набор статических объектов для 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;
Компиляция модуля уложилась в две правки. Символы были настоящей работой
Чтобы модуль бэкенда сам по себе скомпилировался под Free Pascal, понадобилось ровно два изменения: задать диалект ассемблера и заменить конструктор настроек формата на основе записи глобальной переменной по умолчанию. Это честное отражение того, насколько переносим простой Pascal между двумя компиляторами
Символьная сторона стала настоящей работой. Набор объектов ссылается на 176 символов C. Из них 128 уже имели реализации на Pascal внутри модуля и требовали лишь присоединения имён экспорта, поскольку Delphi использует имя функции как имя символа, а Free Pascal требует явного объявления публичного имени. Двадцать семь были общими с кодеком JPEG 2000 и должны были экспортироваться ровно из одного места, ведь двойное определение ломает любую программу, связывающую оба. Оставшиеся 21 были записями платформы и среды выполнения C — шестнадцать файловых функций Win32 плюс несколько вызовов стандартной библиотеки, — и они ушли в новый модуль совместимости
Ничто из этого не сложно концептуально, но всё это необходимо, прежде чем компоновщик хотя бы попытается. На компоновщике всё и остановилось
Три маршрута компоновки, три тупика
Внутренний компоновщик Free Pascal не может читать объектные файлы: они порождены компилятором, выдающим ассоциативные секции COMDAT, а внутренний компоновщик сообщает, что не поддерживает их. Это категоричный отказ, а не предупреждение
Переход на внешний компоновщик выглядел ответом. Поставляемый с Free Pascal компоновщик binutils падает начисто при применении сборки мусора секций к этому архиву, а этот флаг — часть фиксированного набора параметров, который Free Pascal передаёт для 64-битной цели Windows, так что убрать его из командной строки нельзя; документированные ключи его подавления на этом пути игнорируются. Гораздо более новый binutils отказывает иначе: он вообще не может обработать скрипт компоновки Free Pascal, выдавая пустой результат без скрипта и стену ошибок перемещения с ним
О найденном по пути ограничении стоит знать, даже если вы никогда не столкнётесь с проблемой компоновщика. Внешний компоновщик разрешает пути объектных файлов относительно каталога вывода исполняемого файла, а не дерева исходников, поэтому относительная директива включения объекта работает, только когда каталог вывода случайно совпадает с рабочим каталогом компиляции. Библиотека не может делать такое допущение о проекте потребителя, и это само по себе причина предпочесть связываемую библиотеку, а не свободные объектные файлы
Почему другой компилятор C++ не помогает
Очередная очевидная идея — пересобрать сторону C++ компилятором, чьи объекты Free Pascal умеет читать. Это тоже не работает, и причина фундаментальна, а не в ключах. Минимальная единица трансляции C++, содержащая шаблон и скомпилированная со всеми отключёнными возможностями генерации кода, всё равно выдаёт слабые внешние символы: инстанцирование шаблонов и inline порождает их по построению. Free Pascal отвергает этот класс символов начисто. Обратное направление тоже не проходит: основной компоновщик C++ не может потреблять объекты другого компилятора из-за той же обработки секций COMDAT
Поэтому код C++ невозможно доставить Free Pascal в виде объектов ни одним из доступных маршрутов. Его можно доставить как DLL — именно так и произошло: кодировщик и его зависимость обработки изображений собраны в одну библиотеку с двумя плоскими точками входа C, а модуль бэкенда Free Pascal привязывает их динамически и регистрируется точно так же, как статический бэкенд. Путь Delphi и C++Builder не тронут вовсе, и это правильный исход: проблема переносимости одной инструментальной цепочки не должна тревожить цепочку, которая уже работала
Полярность — единственное, что вас укусит
Между двухуровневым растровым изображением Windows и кодировщиком JBIG2 есть расхождение соглашений, которое не поймает никакая система типов. Строка развёртки аппаратно-независимого растра с одним битом на пиксель считает установленный бит белым. Кодировщик считает установленный бит чёрным. Передайте строки без изменений — и получите абсолютно корректный поток JBIG2 с фотографическим негативом вашей страницы
// Однобитный DIB: установленный бит означает белый. Кодировщик JBIG2:
// установленный бит означает чёрный. Инвертируйте каждый байт на входе
for I := 0 to RowBytes - 1 do
Row[I] := Row[I] xor $FF;
Метод проверки значит не меньше самого исправления. Сравнение длин сжатых потоков не говорит ничего: негатив сжимается до похожего размера. Взгляд на страницу доказывает лишь, что она не очевидно инвертирована. Надёжная проверка — отрисовать результат обоих путей кодирования, собственного Pascal и внешнего, в PNG и сравнить их побайтово: оба кодировщика работают без потерь на одном и том же исходном изображении, поэтому всё, кроме точного совпадения, — баг одного из них. Это сравнение стало постоянным регрессионным тестом, и это тот тип утверждения, который стоит строить всякий раз, когда две реализации должны совпадать точно
Какой бэкенд выбрать
Для обычного двухуровневого содержимого — полутонов с дизерингом, штриховой графики, смешанной графики — собственного Pascal-кодировщика MMR достаточно, и он не несёт затрат развёртывания. Для отсканированного текста, того самого случая, ради которого создавался JBIG2, именно внешний кодировщик словарей символов даёт сокращение размера: он выносит повторяющиеся формы глифов в словарь вместо повторного кодирования каждого вхождения. Если вы создаёте архивы отсканированных документов, эта разница достаточно велика, чтобы изменить планирование хранилища
Вопрос выше по течению — как двухуровневое изображение получается в первую очередь — значит для размера результата не меньше; рендеринг монохромных областей рассмотрен в статье о рендеринге монохромных областей, а стратегия размера всего документа — в материале об оптимизации размера PDF и сабсеттинге шрифтов. Для наборов сканов с повторяющимися страницами дедупликация часто выигрывает у лучшего сжатия — этому посвящена перцептуальная дедупликация изображений. Доступность инструментальных цепочек и бэкендов по платформам перечислена на странице продукта losLab PDF Developer Library