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

Кастомные таблицы Huffman JBIG2 в Pascal-декодере PDFlibPas

Высыпьте сегмент JBIG2 Tables (тип 53) побайтно, и первый сюрприз — то, чего в нём нет: сами коды Хаффмана. Вы получаете байт флагов, две границы и серию маленьких чисел, и эти числа — длины кодов. Кодировщик и декодер оба восстанавливают реальные битовые шаблоны из длин по процедуре из ITU-T T.88 Annex B.3. Восстановите их в чуть другом порядке — и вы всё ещё получите валидный префиксный код, каждый шаблон всё ещё декодируется, и каждое значение выходит прицепленным не к тому шаблону

На странице, закодированной символьными ID Хаффмана с таблицами от кодировщика, такое несовпадение выглядит как плохой скан: глифы на пару пикселей в стороне, символы, собранные из не тех символов, или правдоподобные числа, пока ширина не выйдет за диапазон несколькими сегментами позже. Большинство JBIG2 в PDF-файлах — арифметическое, и большинство хаффмановского JBIG2 держится стандартных таблиц B.1–B.15. Файлы со своими таблицами редки, и они обычно нагружают разом парсер таблиц, хаффмановское уточнение и несжатые коллективные битмапы

Что сегмент Tables несёт на самом деле

Сегмент Tables описывает ровно одну таблицу Хаффмана. T.88 §7.4.13 и Annex B.2 раскладывают её как один байт флагов, знаковый 32-битный HTLOW, знаковый 32-битный HTHIGH и затем пары (длина префикса, длина диапазона), которые выстилают интервал от HTLOW до HTHIGH. Бит 0 флагов — HTOOB, он говорит, есть ли у таблицы out-of-band код. Биты 1–3 плюс один дают HTPS — ширину каждого поля длины префикса. Биты 4–6 плюс один дают HTRS — ширину каждого поля длины диапазона. Бит 7 зарезервирован

На границах обычно падает написанный вручную парсер. Прочтите HTLOW как беззнаковый — и таблица, начинающаяся с −2, что обычное дело для дельта-кодированных ширин и высот символов, внезапно заявляет о старте чуть ниже 4.3 миллиарда. PDF Library for Delphi читает обе границы как знаковые, отвергает таблицу, у которой HTLOW не меньше HTHIGH или длина диапазона превышает 32, и сверяет каждое прочитанное поле с битом, где объявленные данные сегмента кончаются. Последняя проверка важнее, чем звучит: таблица, читающая за своим сегментом, проглатывает заголовок следующего сегмента как длины префиксов и разбирается в полную бессмыслицу. Установленный бит 7 валит разбор с сообщением reserved custom Huffman table flag. Мы предпочитаем этот отказ догадке, потому что угаданная таблица даёт неправильные пиксели, которые никто не сможет отследить до причины

После регулярных строк идут эскейп-строки Annex B.2. Нижняя строка диапазона стартует с HTLOW минус один и отсчитывает вниз 32-битным смещением, верхняя строка диапазона стартует с HTHIGH с 32-битным диапазоном, а out-of-band строка существует только при установленном HTOOB. Эти строки несут те же маркеры, что встроенные стандартные таблицы, поэтому своя таблица и Table B.6 проходят через один цикл декодирования. Своя таблица может держать до 65536 строк

Раскладка сегмента Tables за кастомным декодированием Huffman в JBIG2 из PDFlibPas: один байт флагов с HTOOB, HTPS и HTRS и зарезервированным битом, который отклоняется, знаковые границы HTLOW и HTHIGH, набор пар длин префикса и диапазона, а также эскейп-строки jbig2HuffmanLOW, фиксированная 32-битная верхняя строка и необязательная jbig2HuffmanOOB
Каждое поле сегмента читается через хелпер с проверкой границ, потому что таблица, читающая за объявленным концом, проглотила бы заголовок следующего сегмента как длины префиксов, а служебные эскейп-строки совпадают со встроенными стандартными таблицами

Почему префиксные коды нужно присваивать в порядке строк таблицы?

Потому что порядок объявления — единственное, что связывает битовый шаблон с диапазоном значений. Annex B.3 считает, сколько строк используют каждую длину префикса, раздаёт сначала коды длины 1, сдвигает влево для перехода к длине 2 и так далее; внутри одной длины коды достаются строкам в порядке их появления в сегменте. Маленькая таблица делает это конкретным. Шесть строк, помеченные A–F в файловом порядке, с такими длинами префиксов:

СтрокаДлина префиксаКод (файловый порядок)Код, если поменять B и D
A20000
B3100101
C20101
D3101100
E3110110
F0не используетсяне используется

Оба столбца — валидные префиксные коды. Левый — то, что имел в виду кодировщик: со переставленными B и D шаблон 100 даёт диапазон значений D, и декодер продолжает уже с числом из чужого интервала. Строка F имеет длину 0, что B.3 определяет как неиспользуемую; дайте ей однобитный код — и каждое последующее присваивание сдвинется

Лёгкий способ получить правый столбец — универсальная сортировка по длине префикса. TArray.Sort и TList<T>.Sort в Delphi не устойчивы, поэтому равные длины могут вернуться в любом порядке. Для канонических кодов в собственном инструменте идите по строкам в файловом порядке один раз на каждую длину:

uses
  System.SysUtils;

type
  TCodeLine = record
    PrefixLen: Integer;   // как прочитано из сегмента Tables
    Code: Cardinal;       // присваивается ниже
  end;

// Каноническое присваивание по Annex B.3, написано для вашего инструментария.
// Равные длины сохраняют файловый порядок; длина 0 кода не получает.
procedure AssignCanonicalCodes(var Lines: array of TCodeLine);
var
  LenCount: array[0..32] of UInt64;
  FirstCode: array[0..32] of UInt64;
  NextCode: UInt64;
  CurLen, I: Integer;
begin
  FillChar(LenCount, SizeOf(LenCount), 0);
  for I := 0 to High(Lines) do
  begin
    if (Lines[I].PrefixLen < 0) or (Lines[I].PrefixLen > 32) then
      raise Exception.Create('prefix length out of range');
    Inc(LenCount[Lines[I].PrefixLen]);
  end;
  LenCount[0] := 0;
  FirstCode[0] := 0;
  for CurLen := 1 to 32 do
  begin
    FirstCode[CurLen] := (FirstCode[CurLen - 1] + LenCount[CurLen - 1]) shl 1;
    // Проверка Крафта: кодов больше, чем вмещает эта длина
    if FirstCode[CurLen] + LenCount[CurLen] > (UInt64(1) shl CurLen) then
      raise Exception.Create('oversubscribed prefix lengths');
    NextCode := FirstCode[CurLen];
    for I := 0 to High(Lines) do       // файловый порядок разрешает ничьи
      if Lines[I].PrefixLen = CurLen then
      begin
        Lines[I].Code := Cardinal(NextCode);
        Inc(NextCode);
      end;
  end;
end;

Скормите ему шесть строк выше — и A получит 00, C получит 01, B получит 100, D получит 101, E получит 110; F кода не получит. PDF Library for Delphi делает ту же работу проходом подсчёта, который устойчив по построению и не перематывает таблицу 32 раза, и применяет те же две защиты. Переподписанные длины валят разбор с сообщением oversubscribed Huffman prefix codes; пропустить их — и один шаблон может совпасть с двумя строками. Префиксы длиннее 32 бит тоже валят по имени, хотя T.88 на бумаге их разрешает. Мы считаем этот потолок правильным компромиссом: каждый код влезает в одно машинное слово, а файл, которому нужно больше, куда вероятнее повреждён, чем хитёр

Значениям нужна та же аккуратность. Нижняя строка диапазона декодируется как HTLOW минус один минус 32-битное беззнаковое смещение, и с 32-битным промежуточным Integer это вычитание может завернуться в обычный с виду положительный размер. Декодер делает это в 64 битах и поднимает Huffman value exceeds signed integer range, когда истинный результат не влезает

Каноническое присваивание префиксных кодов в декодере JBIG2 из PDFlibPas: в сегменте приходят только длины префиксов, устойчивая сортировка подсчётом по Counts, Starts и Positions сохраняет порядок объявления внутри каждой длины, коды длиной один раздаются первыми и код сдвигается влево на каждую длину, а переполнение отклоняется проверкой Крафта
Кодировщик битовые шаблоны не пишет, поэтому любое отклонение от порядка строк таблицы молча строит другой, но валидный префиксный код, и выход выглядит правдоподобно; строки нулевой длины выпадают как неиспользуемые, а префиксы длиннее 32 бит отклоняются

Порядок селекторов и две ловушки, которые прикрывают друг друга

Текстовый регион или словарь символов не называют свои таблицы. Он несёт двухбитные селекторы, и каждый селектор, выставленный в custom, берёт следующий сегмент Tables из списка referred-to, в порядке referral. T.88 §7.4.3.1.2 упорядочивает селекторы текстового региона как FS, DS, DT, RDW, RDH, RDX, RDY и RSIZE; §7.4.2.1.1 упорядочивает селекторы словаря символов как DH, DW, BMSIZE и AGGINST. Значения 0 и 1 берут стандартные таблицы, 3 — свою, а 2 зарезервировано на полях всего с двумя стандартными таблицами. PDF Library for Delphi отвергает зарезервированные значения селекторов и валит разбор с сообщением missing custom Huffman table reference, когда селекторы хотят больше своих таблиц, чем регион ссылает

Если пишете свой декодер, здесь легко сделать две ошибки, и каждая прячет другую. Первая кусается в Delphi или FPC в режиме Delphi (objfpc-режим отвергает такой код как дубликат идентификатора): сеттер флагов, чей параметр носит то же имя, что поле. Параметр затеняет поле, Flags := Flags присваивает его самому себе, и поле остаётся нулём. Каждый селектор тогда читает ноль, текстовый регион берёт стандартные таблицы B.6, B.8 и B.11 для FS, DS и DT, что бы файл ни просил, и свой путь не запускается вообще

Вторая сидит в словаре символов. Хаффмановский словарь может хранить символы как несжатый коллективный битмап (BMSIZE нулевой), где каждая строка кончается частичным байтом. Значение пэддинга — число валидных битов в этом байте, и соблазнительно прочитать его как позицию младшего валидного бита:

// Последний байт одной строки коллективного битмапа, биты читаются от MSB.
// Padding := TotalWidth mod 8  -> число ВАЛИДНЫХ битов в этом байте

// Неверно: считает Padding младшим индексом бита для копирования
for BitPos := 7 downto Padding do
  CopyPixel((LastByte shr BitPos) and 1);

// Верно: младший валидный индекс бита равен 8 - Padding (0 при Padding = 0)
for BitPos := 7 downto ((8 - Padding) and 7) do
  CopyPixel((LastByte shr BitPos) and 1);

// CopyPixel — что угодно в вашем декодере, что пишет пиксель

Проверьте граничные ширины. Строка в 63 пикселя имеет пэддинг 7: неверный цикл копирует один бит последнего байта, верный — семь. Строка в 9 пикселей имеет пэддинг 1: неверный копирует семь, верный — один. При 64 пикселях оба копируют все восемь, поэтому кратные 8 никогда её не вылавливают. Синтетические коллективные битмапы шириной 7 и 9 пикселей прикалывают обе стороны байтовой границы. И повреждение путешествует дальше, потому что уточнение использует эти символы как референсы

Если в вашем декодере обе ловушки, отладка становится мрачной. Почините один сеттер — свой путь наконец запустится, прямо в битые коллективные битмапы, и страница всё равно выглядит неправильно. Почините один пэддинг — видимых изменений нет, потому что свой путь всё ещё недостижим. Проверяйте оба на синтетических входах, прежде чем доверять любому из изменений. Смежная деталь: хаффмановский словарь символов, у которого входные и новые символы в сумме дают один, получает из формулы log2 длину символьного кода 0, но хаффмановский вариант пишет каждый символьный ID минимум одним битом. PDF Library for Delphi поднимает эту длину до 1 в хаффмановском режиме

Откуда уточнение по Huffman берёт размер своей растровой карты?

Из поля BMSIZE, которое существует только в хаффмановском режиме и декодируется впереди арифметических данных уточнения. Когда экземпляр текстового региона имеет уточнение (RI не ноль) и выставлен SBHUFF, T.88 §6.4.11 велит декодеру прочитать RDW, RDH, RDX и RDY их выбранными таблицами, затем BMSIZE таблицей RSIZE, затем перескочить на границу байта и только потом гонять общее уточняющее декодирование ровно по BMSIZE байтам. У арифметических текстовых регионов такого поля нет, поэтому декодер, делящий один путь кода между режимами, его пропускает, рано запускает арифметический декодер и уточняет каждый символ по шуму. Путь словаря символов с REFAGG и одиночным уточняющим экземпляром (§6.5.8.2.2) имеет то же поле

PDF Library for Delphi ограничивает BMSIZE с обеих сторон. Минимум — 2, начальная пара байтов, которую арифметический декодер потребляет всегда. Максимум — то, что осталось от текущего сегмента, нарочно не всего потока: BMSIZE, который влезает, только позаимствовав байты из следующего сегмента, — признак кривого файла, и проверка по длине всего потока позволила бы арифметическому декодеру читать в следующий заголовок. Размеры вне диапазона валят разбор с сообщением invalid refinement bitmap size. После этого хаффмановский читатель продолжит ровно на BMSIZE байтов дальше поля, что бы там ни успел прочитать арифметический декодер

Границы уточнения в режиме Huffman в декодере JBIG2 из PDFlibPas: RDW, RDH, RDX и RDY декодируются своими таблицами, BMSIZE декодируется таблицей RSIZE и выравнивается на байт, затем арифметический декодер уточняет ровно BMSIZE байт, удерживаемых между RefinementEnd и SegmentEnd, отклоняя размеры меньше двух и выходящие за границу сегмента
Нижняя граница в два байта отражает начальную пару, которую арифметический декодер потребляет всегда, верхняя — это текущий сегмент, а не весь поток, и после уточнения читатель перескакивает на RefinementEnd независимо от забегания вперёд

Объявленная длина сегмента — вот граница

Каждый заголовок сегмента заявляет, сколько байтов данных следует за ним, и декодер держит сегмент в этих пределах в обе стороны: чтение за конец — ошибка, раннее завершение — норма, а следующий заголовок всегда читается оттуда, куда указал файл. Из этого следуют правила поменьше. Неизвестная длина данных из T.88 §7.2.7 отвергается сразу, вместо поиска терминатора. Сегменты referred-to должны нести меньшие номера, чем текущий, и уже существовать. END_OF_PAGE и END_OF_FILE обязаны заявить ноль байтов данных. Сегмент Profiles (тип 52) обязан быть ровно 4 плюс 4 на число идентификаторов; его пропускают, но сохраняют, чтобы поздние сегменты могли на него ссылаться, и неизвестный идентификатор профиля разбор не останавливает

Классический случай раннего завершения — MMR. MMR-декодер закончен, как только выдал последний пиксель последней строки, а это может случиться раньше терминатора EOFB, который T.88 §6.2.5.7 кладёт в конец данных. Читатель, считающий, что он теперь стоит на следующем заголовке, разберёт оставшиеся байты EOFB как номер сегмента и упадёт несколькими байтами позже, далеко от настоящей причины. Прыжок к объявленному концу снимает вопрос. Та же привычка проходит через то, как PDF Library for Delphi вообще разбирает недоверенные PDF-структуры

Декодирование потока изображения JBIG2 из Delphi

Декодер живёт в юните PDFlibJBIG2 как TPLJBIG2Decoder. LoadFromByteArray принимает встроенные JBIG2-данные изображения /JBIG2Decode и, опционально, данные его потока /JBIG2Globals, где обычно живут свои таблицы и разделяемые словари символов. При любом отказе он возвращает False и оставляет причину в LastError; вывод предыдущего вызова очищается в начале каждого, так что проваленный декод никогда не оставит старые пиксели, которые вызывающий примет за результат

uses
  System.SysUtils, PDFlibJBIG2;

// ImageBytes: данные потока изображения, из которых уже убраны
//   все фильтры перед JBIG2Decode
// GlobalBytes: данные потока /JBIG2Globals или пустой массив
function DecodeAndCountBlack(const ImageBytes, GlobalBytes: TJBIG2ByteArray;
  out BlackPixels: Int64): Boolean;
var
  Decoder: TPLJBIG2Decoder;
  Line: TJBIG2ByteArray;
  Row, Col: Integer;
begin
  BlackPixels := 0;
  Decoder := TPLJBIG2Decoder.Create;
  try
    Result := Decoder.LoadFromByteArray(ImageBytes, GlobalBytes);
    if not Result then
    begin
      // Формат: 'JBIG2 segment <n> (type <t>) at byte <offset>: <reason>'
      Writeln('JBIG2 refused: ', Decoder.LastError);
      Exit;
    end;
    for Row := 0 to Decoder.Height - 1 do
    begin
      Decoder.GetScanline(Row, Line);   // один байт на пиксель, 0 или 1
      for Col := 0 to Decoder.Width - 1 do
        Inc(BlackPixels, Line[Col]);    // 1 = чёрный в JBIG2
    end;
  finally
    Decoder.Free;
  end;
end;

Счёт чёрных пикселей — ваша собственная проверка, дешёвый отпечаток для сверки со вторым рендерером. Сегментные отказы приходят с префиксом из номера сегмента, типа и байтового смещения, поэтому (type 53) перед сообщением oversubscribed Huffman prefix codes показывает прямо на сегмент Tables. Держите два канала ошибок раздельно: TPLJBIG2Decoder.LastError — диагностический текст этого декодера, а TPDFlib.LastErrorCode — числовой код API уровня библиотеки. Выбор страницы следует за потоком: декодер возвращает битмап первого встреченного сегмента page-information и не ищет ассоциацию страницы 0, потому что JBIG2 внутри PDF несёт одну страницу, и кодировщики обычно нумеруют её единицей

Сравнение рендереров на таких файлах требует некоторой скромности, потому что независимые декодеры JBIG2 не всегда согласны по файлам со своими таблицами и хаффмановским уточнением. Два несовпадающих декодера ничего не решают. Решают поля формата: какие таблицы выбрали селекторы, что заявил BMSIZE, где кончается каждый сегмент

Кодирование разобрано в статье про бэкенды JBIG2-энкодера и то, как они линкуются. Путь чтения обязан принимать то, что выдал чужой энкодер, и делит правила с встроенным TIFF-декодером и его проверками BigTIFF и tiled-раскладки: отказ по имени, никаких заимствований байтов за объявленной границей, промежуточная арифметика в широкой разрядности. Если папка отсканированных PDF постоянно валит другие тулкиты, проверьте, не JBIG2 ли это с хаффмановским кодированием и своими таблицами, и прогоните их через нативный путь чтения Delphi и C++Builder, описанный на странице продукта PDF Library for Delphi