Техническая статья

Добавление изображений JPEG 2000 в PDF на Delphi с помощью HotPDF

Отсканированный медицинский слайд, тайл аэрофотосъёмки, кадр из фильма, заархивированный в полном динамическом диапазоне. Именно такие изображения поступают в формате JPEG 2000, и на то есть веская причина. Формат сохраняет 12 или 16 бит на канал, использует для сжатия вейвлет-преобразование вместо блочного DCT, применяемого в JPEG, и может кодировать одно и то же изображение как без потерь, так и с потерями из одного кодового потока. Когда документ, созданный из таких источников, должен стать PDF, изображение должно пройти через фильтр, который спецификация PDF зарезервировала именно для этого кодека

HotPDF v2.228.0 восстановил работающий движок декодирования JPEG 2000 для этого пути. В более ранней версии модуль поставлялся со функциями-заглушками, возвращавшими nil, то есть API существовал, но ничего не декодировал. Текущий движок статически связывает OpenJPEG 2.5.4 и преобразует исходный файл JP2 или J2K в пиксели, которые HotPDF может разместить на странице

Фильтр JPXDecode в PDF

ISO 32000-1 определяет фильтр JPXDecode в §7.4.9. PDF image XObject указывает своё сжатие в записи /Filter словаря потока, и JPXDecode - это значение, означающее, что данные потока представляют собой кодовый поток JPEG 2000, а не базовый JPEG, который несёт /DCTDecode. Фильтр позволяет PDF хранить данные изображения, сжатые с помощью вейвлета, с высокой битовой глубиной, и допускает как режим без потерь, так и режим с потерями кодека, поскольку режим является свойством самого кодового потока, а не обёртки вокруг него

Последний момент заслуживает особого внимания. JPEG 2000 - это единый алгоритм с особым случаем без потерь, а не два отдельных формата. Обратимый вейвлет 5/3 восстанавливает исходные сэмплы точно; необратимый вейвлет 9/7 жертвует этой точностью ради меньшего файла. Декодер обрабатывает оба варианта одинаково при чтении - именно поэтому HotPDF нужен только один путь декодирования для приёма любого потока JPXDecode

Что декодер делает с пикселями

PDF image XObjects в типичном случае ожидают 8 бит на компоненту в DeviceGray или DeviceRGB. JPEG 2000 регулярно превышает этот порог, а его модель компонент более обобщённая, чем упакованный растр, поэтому декодер должен выполнить три задачи, прежде чем данные станут пригодным обычным изображением

Во-первых, компоненты с высокой битовой глубиной передискретизируются до 8 бит. 12-битный или 16-битный сэмпл масштабируется вниз до диапазона от 0 до 255, чтобы результат представлял собой обычный 8-битный растр. Знаковые компоненты сначала переводятся в беззнаковый диапазон. Важна деталь: само по себе это является потерей - 16-битный градации серого скан теряет свой глубокий тональный диапазон в момент превращения в 8-битное PDF-изображение, что является правильным компромиссом для экранного и печатного вывода, но не для повторного архивирования

Во-вторых, цветовое пространство YCbCr (в кодеке оно называется SYCC) преобразуется в RGB. JPEG 2000 часто хранит цвет в пространстве яркость-цветность для эффективности сжатия - та же идея, что использует базовый JPEG, - и декодер применяет стандартное обратное преобразование, чтобы страница получила настоящий RGB

В-третьих, субдискретизированные компоненты апсэмплируются методом ближайшего соседа. Каналы цветности часто хранятся в половинном разрешении, поэтому декодер читает каждую компоненту с её собственными размерами и собственным коэффициентом дискретизации, затем тиражирует сэмплы, чтобы поднять каждый канал до полного размера изображения, перед межканальным чередованием. Метод ближайшего соседа сохраняет этот шаг дешёвым; заполняемая цветность изначально низкочастотная, поэтому видимые потери невелики

Боксы JP2 в сравнении с необработанным кодовым потоком J2K

Файл JPEG 2000 бывает двух форм, и HotPDF определяет, какую из них он читает, по первым байтам, а не по расширению файла. Файл JP2 представляет собой контейнер с боксовой структурой: он открывается двенадцатибайтовым боксом подписи 00 00 00 0C 6A 50 20 20 и оборачивает кодовый поток боксами, описывающими цветовое пространство, разрешение и метаданные. Необработанный кодовый поток J2K не имеет контейнера вообще и начинается с маркера SOC FF 4F FF 51. Декодер читает эти ведущие байты, распознаёт сигнатуру и выбирает соответствующий кодек OpenJPEG для каждого случая

Обе формы обрабатываются, поскольку обе встречаются на практике. Устройства захвата и архивы, которым нужны метаданные, выдают JP2; инструменты, стремящиеся к минимальному объёму, выдают голый кодовый поток. Тип формата моделируется как перечисление TJpeg2000FileType с членами jtInvalid, jtJP2, jtJ2K и jtJPT. Элемент JPT обозначает потоковый вариант JPIP; детектор байтовых сигнатур распознаёт две формы, которые он может декодировать - JP2 и J2K - и сообщает об остальном как jtInvalid, чтобы неподдерживаемые входные данные завершались корректно, а не производили мусор

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 или J2K, автоопределение
      if Decoder.GetImageData(Pixels) then
        // Pixels содержит 8-битные данные с чередованием, шириной ColorComponents каналов,
        // построчно сверху вниз: готово к использованию в DeviceGray/DeviceRGB XObject.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

Режимы без потерь и с потерями на стороне кодирования

Декодер читает оба режима, не зная заранее, какой из них используется. Выбор становится параметром только тогда, когда вы идёте в обратном направлении и создаёте файл JPEG 2000, что HotPDF также умеет делать через класс TJpeg2000Bitmap - потомок TBitmap, который загружает и сохраняет растровые данные в формате JP2. Два свойства управляют выводом. LosslessCompression - булево свойство, выбирающее обратимый вейвлет при значении true; CompressionQuality - это TJpeg2000QualityRange, целое число от 1 до 100, где 1 означает маленький и некачественный, а 100 - большой и точный. Значения по умолчанию хранятся в именованных константах: Jpeg2000DefaultLosslessCompression равно False, Jpeg2000DefaultLossyQuality - 80

Это решение принимается исходя из содержимого. Режим без потерь подходит для мастер-копии, медицинского или юридического скана, всего, что может быть повторно закодировано позднее и не должно накапливать поколенческие потери. Режим с потерями при качестве 80 подходит для изображения, предназначенного для экрана или печати, где плавная деградация вейвлета даёт заметно меньший файл без артефактов, которые заметит читатель. Стоит упомянуть одну оговорку для CMYK: растр предоставляет метод SetCMYK для маркировки четырёхканальных данных как CMYK, что важно для конвейеров печати, сохраняющих разделения

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // декодирование существующего JP2/J2K
    Bmp.LosslessCompression := True;         // обратимый вейвлет 5/3
    // или, для получения файла меньшего размера с потерями:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // совпадает со значением по умолчанию
    Bmp.SaveToStream(Output);                // всегда записывает файл JP2
  finally
    Bmp.Free;
  end;
end;

Почему отсутствует конвейер фильтрации при загрузке

Один архитектурный факт определяет порядок работы со всем вышеперечисленным, и легко предположить обратное. В HotPDF нет общего фильтра декодирования изображений при загрузке. Когда вы открываете PDF, уже содержащий изображение JPXDecode, движок не декодирует этот поток. Он сохраняет байты JPEG 2000 точно в том виде, как они есть, поэтому копирование страницы или слияние документа пропускает изображение нетронутым, байт в байт. У декодера единственная точка входа, и она находится на стороне создания: файловый метод AddImage, диспетчеризуемый по расширению файла для обработки источников .jp2, .j2k, .jpt и .jpc

Такое разделение является правильным архитектурным решением, а не ограничением. Декодирование встроенного потока JPX при загрузке только ради его повторного кодирования при сохранении превратило бы архивное изображение без потерь в изображение с потерями и раздуло бы каждое слияние - всё ради изображения, которое вы намеревались просто переместить из одного PDF в другой. Сквозная передача потока является операцией без потерь и быстрой. Декодирование откладывается до единственного момента, когда оно действительно необходимо: когда вы передаёте движку файл JPEG 2000 с диска и просите растеризовать это изображение для размещения на новой странице. В этот момент файл должен стать пикселями, и декодер запускается

Регистрация поддержки и размещение изображения

Регистрация изображений JPEG 2000 является опциональной за переключателем компиляции HPDF_REGISTER_JPEG2000_PICTURE, который по умолчанию отключён. Причина - реальный конфликт, а не осторожность: глобальная регистрация форматов jp2, j2k и jpc через TPicture может мешать обнаружению формата BLOB, на которое опирается TppDBImage из ReportBuilder. Определите переключатель, когда эта интеграция не задействована - форматы зарегистрируются и TPicture будет их распознавать; оставьте его неопределённым - диспетчеризация по расширению в AddImage по-прежнему декодирует файлы JPEG 2000 напрямую, поскольку этот путь вообще не проходит через TPicture

Разобравшись с этим, размещение изображения JPEG 2000 следует тому же трёхвызовному ритму, что и любое другое изображение HotPDF. Передайте AddImage путь к .jp2 и тип сжатия для хранения изображения в выходном файле, затем разместите возвращённый индекс изображения на странице с помощью ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // Источник .jp2 декодируется через бэкенд OpenJPEG, а затем
    // повторно внедряется с тем сжатием, которое вы здесь запросили.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, ширина, высота в пунктах; финальный 0 — угол поворота.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Сжатие, передаваемое в AddImage, управляет тем, как декодированное изображение повторно сохраняется, а не тем, как оно было прочитано. Файл JPEG 2000, декодированный в растр, может быть сохранён обратно как DCTDecode JPEG, растр Flate или другой поддерживаемый фильтр - в зависимости от того, что подходит документу. Декодирование из JP2 или J2K происходит в первую очередь в любом случае, поэтому один и тот же вызов принимает источник, сжатый с помощью вейвлета, и встраивает его в той форме, которую ожидает остальная часть конвейера

Общую картину того, как изображения и шрифты попадают в генерируемый вывод, смотрите в наших заметках о выводе отчётов с шрифтами и изображениями. Когда собираемый документ повторно использует содержимое существующих PDF, описанное здесь поведение сквозной передачи сочетается с механикой слияния и ревизий в статье потоки объектов и инкрементные обновления. Движок декодирования JPEG 2000 входит в состав компонента HotPDF для Delphi и C++Builder, рядом с API изображений, шрифтов и документов, рассмотренными в других статьях блога