Поток, помеченный /Predictor 12, не означает, что каждая строка использует PNG-фильтр 2. HotPDF, нативный VCL-компонент PDF для Delphi и C++Builder, трактует значения предиктора от 10 до 15 как одно семейство: настоящий тег фильтра, от 0 до 4, — это первый байт каждой закодированной строки, и HPDFDecodePredictor читает и валидирует этот тег построчно. Это различие — форма почти каждого бага в этом уголке PDF, потому что ничто не сигнализирует, когда вы ошибаетесь. Цепочка фильтров выполняется, растр имеет ожидаемый размер, а изображение выходит как диагональные помехи или градиент, всё дальше уплывающий с каждой строкой сканирования. Пять чисел в /DecodeParms (ISO 32000-1 §7.4.4) по большей части меняют смысл байтов, а не их длину, так что неверное значение производит правдоподобный мусор вместо ошибки
Почему /Predictor 12 не означает PNG-фильтр 2 на каждой строке?
Потому что номер предиктора говорит только «используется PNG-предсказание», а не какой фильтр. Кодеры PNG выбирают фильтр на строку сканирования, и фильтр PDF наследует это, так что значения предиктора 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) и 15 (Optimum) декодируются одинаково: ведущий байт-тег каждой строки — это то, чему обязан подчиняться декодер. Следствие для разметки не менее важно, чем семантика. Каждая закодированная строка — 1 + RowBytes байт длиной, вход поэтому превышает выход ровно на число строк, а поток, чья длина не кратна целиком RowBytes + 1, по определению усечён. HotPDF проверяет эту границу прежде, чем коснуться байта, отклоняет любой тег выше 4 с Invalid PNG predictor row tag и читает предыдущую строку прямо из единого выходного буфера вместо материализации двумерного массива строк. Фильтры 1 и 3 обращаются назад на BytesPerPixel внутри текущей строки, фильтр 2 читает прямо вверх, фильтр 4 запускает выбор Paeth по левому, верхнему и верхне-левому — и все четыре оперируют уже реконструированным выводом, поэтому верхняя строка обязана быть декодированной строкой и никогда не отфильтрованным вводом
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
Аргумент MaxOutputBytes — не украшение. Стадия предиктора — это стадия декомпрессии в маскировке, и враждебное или просто битое значение /Columns превращает несколько килобайт входа в запрос на выделение памяти в несколько гигабайт. HotPDF вычисляет биты на строку, байты на строку и общий размер растра сначала в Int64, отказывается от геометрии, вызывающей переполнение, и соблюдает потолок, заданный вызывающей стороной. Передайте реальную границу, выведенную из словаря изображения, и режим сбоя становится записанным в журнал сообщением, а не диалогом нехватки памяти на машине клиента
Почему TIFF Predictor 2 портит 4-битные изображения?
Потому что Predictor 2 — это горизонтальное дифференцирование по сэмплу, а не по байту, а при 1, 2 или 4 битах на компонент несколько сэмплов делят один байт. Обычная реализация прибавляет байт N-Colors к байту N, что оказывается верным при 8 битах на компонент и молча неверно везде ещё. 8-битное RGB сканирование декодируется идеально, затем тот же код разрушает 4-битное индексированное изображение при первом же его появлении в продакшне
Корректная арифметика работает внутри битового поля. HotPDF обходит сэмплы от индекса Colors до Colors * Columns - 1, извлекает сэмпл и его соседа слева того же компонента с маской (1 shl BitsPerComponent) - 1 на соответствующем сдвиге, складывает их по модулю этой маски и записывает результат обратно, не тревожа другие сэмплы, упакованные в тот же байт. Хвост важен не меньше: строка дополняется до границы байта, так что биты дополнения после последнего сэмпла должны выжить нетронутыми, а не быть свёрнутыми в арифметику. При 16 битах на компонент каждый сэмпл — это пара байтов big-endian, и сложение переносится через $FFFF внутри пары, а не переносится между байтами независимо; при 8 битах простая рекуррентность байтов верна, с шагом Colors, так что красный накапливается против красного, а альфа против альфы. В каждом варианте первый пиксель строки — это литерал, никогда разность, и рекуррентность перезапускается на каждой границе строки — TIFF-предсказание никогда не читает строку выше, что и есть вся разница между ним и семейством PNG
Что на самом деле контролирует EarlyChange в LZWDecode?
Он контролирует, когда читатель расширяет размер кода на один бит, и опоздание на один код на шаг портит всё, что следует дальше. HotPDF выражает правило как один инвариант: после добавления записи в словарь следующее чтение расширяется, когда NextCode достигает (1 shl CodeSize) - Ord(EarlyChange). С /EarlyChange 1, значением по умолчанию ISO 32000-1 §7.4.4, переключение происходит на один код раньше; с /EarlyChange 0 оно происходит точно на границе. Оба встречаются в реальных файлах, и ничто в битовом потоке не говорит, какой из них использовал кодер. Остальная часть конечного автомата должна двигаться синхронно: код очистки сбрасывает размер кода, битовую маску, следующий свободный код и хранилище фраз вместе, а код конца информации читается на той ширине, что текущая в этот момент, а не на исходных 9 битах. HotPDF стартует с InitialCodeSize 9, ограничивает размер кода 12, а словарь — 4096 записями, и по умолчанию задаёт FillOrder как foTop, потому что PDF упаковывает коды старшим битом первым — foBottom существует для потоков в стиле TIFF, которые этого не делают
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Статистика существует для диагностики, а не для тщеславия. Когда файл декодируется в верную длину, но с неверными пикселями, PeakCodeSize и DictionaryAdds сразу же говорят вам, расширялся ли читатель там же, где и писатель. Переключите EarlyChange, декодируйте снова, сравните оба: если числа сдвигаются, у вас есть ответ за один прогон вместо пошагового прохода по битовому читателю
Ветвь KwKwK, и когда потоку следует просто провалиться
Единственный законный случай, выглядящий незаконным, — это Code = NextCode, и HotPDF обрабатывает его, строя запись прежде, чем её выпустить. Кодер может выпустить код фразы, которую он определяет на том же шаге, что случается всякий раз, когда вход содержит паттерн формы K w K w K; декодер не может найти этот код, потому что он ещё не существует, поэтому он должен построить Previous + First(Previous), добавить это как новую запись и выпустить запись, которую только что создал. HotPDF считает их в KwKwKExpansions и перепроверяет, что добавленный код — это код, который был запрошен. Всё выше NextCode — это повреждение, и здесь декодеру следует остановиться, а не импровизировать: HotPDF выбрасывает исключение на будущем коде, на префиксе словаря, указывающем за пределы арены фраз, на переполненном словаре и на первом коде, который не является литералом. Два переключателя строгости намеренно выключены по умолчанию, RequireInitialClear и RequireEndOfInformation, потому что множество продакшн-PDF пропускают ведущий код очистки или заканчивают данные без терминатора. Включайте их при валидации собственного вывода, оставляйте выключенными при потреблении файлов из дикой природы
Где /DecodeParms на самом деле читается со стороны загруженного документа
HotPDF разрешает /DecodeParms или его сокращение /DP в словаре потока изображения, принимает либо словарь, либо массив и берёт последний элемент, когда это массив, затем переносит Predictor, Colors, BitsPerComponent, Columns и EarlyChange в путь растра. Случай с массивом — тот, что люди забывают: поток, отфильтрованный через [/ASCII85Decode /FlateDecode], несёт параллельный массив параметров, и настройки предиктора принадлежат последнему фильтру, а не первому
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Один исторический дефект на этом пути стоит назвать, потому что этот класс бага повторяется. Старая процедура Flate с параметрами создавала поток декомпрессии, а затем копировала из исходного сжатого ввода, так что стадия предиктора получала сжатые байты и добросовестно их «распредсказывала»: всегда неверно, никогда не сигнализировалось. Текущий код читает только из декодера, прежде чем передать результат общему предиктору, и отклоняет растр короче вычисленного размера вместо отката на всё ещё сжатые байты — откат, который раньше превращал сбой декодирования в повреждённый битмап. Та же реализация предиктора теперь обслуживает и потоки перекрёстных ссылок, что полезная согласованность, если вы также работаете с потоками объектов и инкрементальными обновлениями, а окружающая механика извлечения покрыта в сопутствующей статье об извлечении загруженных изображений и их фильтров декодирования. Изображения, приходящие как DCTDecode или JPXDecode, никогда не достигают предиктора вообще; они несут собственную сжатую модель пикселей
Пропускная способность: непрерывная арена фраз против строк на запись
Замена словаря строк на запись непрерывной ареной фраз показала примерно в 1,61 раза быстрее на патологическом вводе: 1558 МиБ/с против 969 МиБ/с на бенчмарке, чья единственная самая длинная фраза достигает 7 370 880 байт. Форма этого ввода объясняет разрыв, потому что классические реализации выбирают одно из двух плохих компромиссов. Словарь значений AnsiString выделяет и копирует свежую строку для каждой из до 4096 записей, каждая новая запись копирует своего родителя целиком; стек префиксов/суффиксов избегает этой памяти вовсе, но реконструирует каждую фразу, обходя цепочку назад байт за байтом и разворачивая её, что нормально для обычного текста и мучительно, когда одна фраза достигает мегабайт. HotPDF дописывает каждую фразу непрерывно в геометрически растущую арену, индексирует записи по смещению и длине и выпускает фразу единственным Move в выходной буфер. Честная цена — память: арена, держащая каждую фразу целиком, ограничена суммой всех длин фраз, а не числом записей, что именно и есть причина, почему MaxOutputBytes существует и на декомпрессоре, и на предикторе. Выведите этот предел из того, что словарь изображения заявляет о том, каким должен быть растр, и лживый поток проваливается быстро
LZW-декомпрессор, общий предиктор и путь извлечения загруженного изображения, показанные здесь, поставляются как часть стандартного компонента HotPDF для Delphi и C++Builder, с полным справочником по фильтрам и DecodeParms на странице продукта