HotPDF, нативный PDF-компонент для Delphi, декодирует изображения CCITTFaxDecode с полной поддержкой расширения uncompressed-mode из T.4/T.6 и восстановления повреждённых строк, управляемого параметром DamagedRowsBeforeError. В uncompressed mode входит только расширительный код, оканчивающийся на 111, а повреждённые строки чинятся только для данных Group 3 с установленным EndOfLine. Всё остальное чисто падает вместо того, чтобы выдать перекошенную страницу
Факсовые изображения — длинный хвост декодирования изображений в PDF. Большинство встреченных CCITT-потоков вышли из драйвера сканера или конвейера захвата документов, выдающих только обычный Group 4, и декодер, который обрабатывает общий случай, проходит любые ваши тесты. А потом приходит архив с пятнадцатилетнего факс-сервера, страница отрисовывается диагональным шумом от 400-й строки вниз, и вы обнаруживаете, что те две части рекомендаций ITU-T, которые никто не реализует, — ровно те две части, что использует этот файл. Эта статья проходит по этим двум частям так, как их обрабатывает HotPDF, и по местам, где декодер может ошибиться, продолжая выглядеть правильным
Какие параметры CCITTFaxDecode реально меняют битовый поток?
Четыре записи словаря DecodeParms меняют то, как декодер нарезает битовый поток: K, EndOfLine, EncodedByteAlign и DamagedRowsBeforeError. Их определяет ISO 32000-1 §7.4.6 (таблица 11), и ошибка в любой из них сдвигает каждую строку после первого несовпадения. BlackIs1, Columns, Rows и EndOfBlock тоже важны, но они влияют на полярность, геометрию и место остановки декодирования, а не на то, как находится граница строки
Kвыбирает схему кодирования: отрицательный — чисто двумерный Group 4 (T.6), ноль — одномерный Modified Huffman (Group 3 1D), положительный — смешанный Group 3 2D, где бит-тег после каждого EOL говорит, будет следующая строка 1D или 2DEndOfLineговорит, что каждая строка Group 3 предваряется кодом EOL — одиннадцатью нулевыми битами и единицейEncodedByteAlignговорит, что каждая закодированная строка начинается на границе байта, и декодер должен выбросить биты выравнивания перед её чтениемDamagedRowsBeforeError— сколько повреждённых строк декодер может стерпеть, прежде чем сдаться; по умолчанию 0
Все эти параметры HotPDF читает из словаря изображения при вызове THotPDF.ExtractLoadedImage или THotPDF.DecodeLoadedImageStream. Об одном намеренном отклонении стоит знать: когда Columns отсутствует, HotPDF откатывается к Width изображения, а не к дефолту спецификации 1728, а при отсутствии Rows берёт Height. Тот, кто опустил Columns на A4-скане шириной 2480 пикселей, неправ, но такое встречается достаточно часто, чтобы доверие к геометрии изображения давало пригодную страницу там, где буквальный дефолт дал бы мусор
Почему 000000 нельзя считать концом блока?
В двумерных строках шесть ведущих нулевых битов двусмысленны, и декодер, читающий их как начало EOFB, примет корректный вход в uncompressed mode за конец изображения. В Group 4 между строками нет EOL; единственные коды с обилием нулей — это EOFB, то есть два EOL подряд, и расширительные коды из ITU-T T.4 и T.6. Оба начинаются с 000000, и расходятся они на седьмом бите
CCITT-декодер HotPDF разбирает префикс бит за битом. Седьмой бит 1 означает, что следующие три бита — расширительный код, полный паттерн 0000001xxx. В uncompressed mode входит только xxx = 111; любое другое расширительное значение трактуется как некорректное, потому что факс-фильтру PDF остальные не нужны. Седьмой бит 0 может лишь продолжать EOL, поэтому декодер затем требует ещё четыре нулевых бита и замыкающую 1 — всего одиннадцать нулей и единица, — прежде чем сообщить EOFB. Всё короче или длиннее — ошибка, а не снисходительный конец изображения
Снисходительная версия соблазнительна, потому что делает целый класс битых файлов благополучными на вид: декодирование просто рано останавливается, и вы получаете короткое изображение без исключения. Но это ровно тот баг, который обрезает легитимные факсы на первом же участке несжатых данных, а на отсканированном бланке это обычно плотная подпись или печать. HotPDF выбирает строгое чтение, поэтому короткое изображение означает, что кодировщик действительно там закончил
Как uncompressed mode кодирует пиксели?
Uncompressed mode заменяет кодирование длин серий буквальными пиксельными паттернами, каждое кодовое слово несёт не больше пяти пикселей, пока выходной код не переключит обратно. Одномерные строки входят в него 12-битным кодом 000000001111; двумерные используют описанное выше 10-битное расширение 0000001111. Кодировщики переключаются на него в областях, похожих на растр, где серии длиной в один-два пикселя стоили бы дороже самих пикселей
Внутри режима кодовые слова считаются по ведущим нулям. От нуля до четырёх нулей и единица означают столько белых пикселей и затем один чёрный. Пять нулей и единица — пять белых пикселей и ничего больше. От шести до десяти нулей и единица — выходные коды: нули сверх шести выдают до четырёх дополнительных белых пикселей, а единственный бит после выходного кода, бит-тег, задаёт цвет следующей серии, когда обычное кодирование серий возобновится
{ Кодовые слова uncompressed mode (ITU-T T.4 / T.6)
W = белый пиксель, B = чёрный пиксель, T = бит-тег
1 B
01 W B
001 W W B
0001 W W W B
00001 W W W W B
000001 W W W W W
0000001T выход, затем серии продолжаются с цветом T
00000001T W, выход
000000001T W W, выход
0000000001T W W W, выход
00000000001T W W W W, выход
больше десяти нулей: некорректно }
Бит-тег — самая тонкая часть. Кодирование серий хранит состояние: строка 1D чередует белые и чёрные серии, а строка 2D отслеживает цвет текущего изменяющегося элемента a0. После выходного кода декодер обязан продолжить тем цветом, который называет тег, а он может и не совпасть с последним буквальным пикселем. Декодер HotPDF фиксирует смену цвета в текущей позиции, когда тег отличается от отслеживаемого цвета, так что следующая серия стартует с правильного цвета. Проигнорируете тег — остаток строки выйдет инвертированным по цвету, а в 2D-режиме каждая следующая строка унаследует повреждение через свою референсную линию
Что восстанавливает DamagedRowsBeforeError, а что нет?
DamagedRowsBeforeError позволяет HotPDF заменить повреждённую строку и продолжить, но только когда K равен 0 или положителен, а EndOfLine истинен. Эти два условия не произвольны. Восстановлению нужен способ найти, где начинается следующая строка, после того как биты перестали иметь смысл, а единственный надёжный ориентир в CCITT-потоке — код EOL. Данные Group 4 не имеют EOL вовсе, а данные Group 3 без обязательных EOL не дают гарантии, что он существует, поэтому в обоих случаях повреждённая строка завершает декодирование независимо от значения параметра
Когда строка не удалась, декодер HotPDF сканирует вперёд до следующего EOL — серии не менее чем из одиннадцати нулевых битов и единицы — и подставляет строку-замену. Первая повреждённая строка после хорошей заменяется копией предыдущей, что на отсканированной текстовой странице обычно достаточно близко, чтобы быть незаметным. Повреждённая строка, идущая сразу за другой повреждённой, заменяется полностью белой строкой, потому что копия копии размазала бы одну строку текста в высокую чёрную полосу. Замена затем становится референсной линией для следующей 2D-строки — именно это сохраняет декодируемость потока смешанного режима после починки
Две границы легко пропустить. Бюджет кумулятивен по всему изображению, а не на серию плохих строк, так что DamagedRowsBeforeError 3 терпит три повреждённые строки суммарно, а четвёртая валит декодирование. И неудавшийся декод в HotPDF — всё или ничего: THotPDF.ExtractLoadedImage возвращает nil, а не битмап с рваным нижним краем. THPDFLoadedImageInfo.Decodable сообщает, что фильтр поддерживается, а не что именно этот поток цел, поэтому всегда проверяйте битмап на nil
Почему скан обязан остановиться в начале EOL
Скан ресинхронизации оставляет читателя на первом нулевом бите EOL, а не после него. У декодера один цикл нарезки: он поглощает EOL, проверяет двойной EOL, завершающий блок, и читает бит-тег смешанного режима. Если восстановительный скан проглотит сам EOL, тот цикл увидит строку без EOL, сочтёт её тоже повреждённой, сожжёт вторую единицу бюджета и заодно выкинет хорошую строку. Хуже того, пропущенный бит-тег означает, что 2D-строка будет декодирована как 1D или наоборот. То же правило действует, когда 1D-декодер серий натыкается на неожиданный EOL посреди строки: он отматывается к началу кода серии, чтобы маркер остался на месте для восстановительного скана
Инвентаризация CCITT-изображений в загруженном PDF
Прежде чем доверять партии архивных факсов, выясните, какие изображения используют CCITTFaxDecode и какие не декодируются. THotPDF.GetLoadedImageInfo даёт геометрию и имя фильтра, ничего не декодируя, а THotPDF.ExtractLoadedImage показывает, выжил ли поток. О том, как диспетчер изображений маршрутизирует каждый фильтр в целом, смотрите руководство по извлечению загруженных изображений PDF и их фильтров декодирования
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
Exit;
for I := 0 to Pdf.GetLoadedImageCount - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
Continue;
Bmp := Pdf.ExtractLoadedImage(I); // nil, когда поток повреждён сверх меры
try
if Bmp = nil then
Writeln(Format('obj %d: %dx%d CCITT failed to decode',
[Info.ObjectNumber, Info.Width, Info.Height]))
else
Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
finally
Bmp.Free; // битмапом владеет вызывающий
end;
end;
finally
Pdf.Free;
end;
end;
Для неудач сохраняйте улики. THotPDF.ExtractLoadedImageToStream в режиме по умолчанию lismEncoded пишет сохранённые байты потока нетронутыми — именно это стоит передать второму декодеру, завернуть в TIFF-контейнер для разглядывания или приложить к баг-репорту. Перекодирование повреждённого факса лишь прячет место повреждения
procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
finally
Fs.Free;
end;
end;
Если вы сами проходите потоки изображений через THotPDF.CollectLoadedImageStreams, передавайте каждый THPDFStreamObject в THotPDF.DecodeLoadedImageStream — и вы получите тот же CCITT-путь, те же параметры и правила восстановления. Освобождайте возвращённый список, но не объекты потоков в нём: ими владеет документ
Стоит ли поднимать DamagedRowsBeforeError на чужих файлах?
Нет — ни правкой PDF, а обычно и вовсе никак. Параметр принадлежит производителю, и только он знает, теряет ли биты его канал передачи. Файл, объявляющий DamagedRowsBeforeError 0 и содержащий повреждённую строку, говорит вам, что архивная копия испорчена, и честный результат — провал декодирования, который можно занести в журнал. Молчаливая починка строк в договоре или медицинской карте превращает обнаружимый дефект в правдоподобно выглядящее изображение с пропавшей строкой, а это исход хуже для всякого, кто потом на него обопрётся
Безопаснее для недоверенных архивов держать декод строгим, записывать, какие номера объектов провалились, и направлять их человеку или отдельному декодеру. Если архив велик и собран из многих источников, вынос декодирования изображений в отдельный процесс ограничивает то, что враждебный поток сможет сделать с хостовым приложением; статья о изоляции кодеков изображений при обработке недоверенных PDF описывает такую схему, а руководство по bilevel-сжатию JBIG2 сравнивает второй bilevel-фильтр, который попадётся в тех же отсканированных архивах
FAQ
Поддерживает ли HotPDF uncompressed mode CCITT в изображениях Group 4?
Да. HotPDF распознаёт 10-битное расширение 0000001111 в двумерных строках и 12-битный входной код 000000001111 в одномерных, декодирует буквальные кодовые слова и учитывает выходной бит-тег при возобновлении кодирования серий
Почему DamagedRowsBeforeError не действует на мой факс Group 4?
Восстановлению нужны маркеры EOL для ресинхронизации. HotPDF применяет DamagedRowsBeforeError только когда K равен 0 или положителен, а EndOfLine истинен; данные Group 4 (отрицательный K) не имеют EOL, поэтому любая повреждённая строка завершает декод
Что возвращает ExtractLoadedImage для повреждённого CCITT-изображения?
THotPDF.ExtractLoadedImage возвращает nil, когда повреждение превышает допустимое DamagedRowsBeforeError или вообще не поддаётся восстановлению. Частичный битмап он не вернёт никогда, поэтому проверяйте на nil и используйте ExtractLoadedImageToStream, чтобы сохранить сырые байты
HotPDF обрабатывает изображения CCITT, JBIG2, JPEG, JPEG 2000 и Flate в загруженных PDF из обычного кода Delphi и C++Builder, без внешних DLL для факсового пути. Подробности и издания — на странице компонента HotPDF Delphi PDF