HotPDF открывает три кернела даунсэмплинга изображений через свойство ImageDownsampleKernel и отдельный проход Floyd-Steinberg через RenderOutputDither. Первый управляет тем, как выглядят фотографии после уменьшения под бюджет размера, второй — как они выглядят после приведения страницы к чёрно-белому. Ни один не включён по умолчанию, и оба опциональны по одной причине: они стоят реального времени
Давление, приводящее сюда, знакомо. Скан контракта на 60 МБ надо пропустить через почтовый шлюз, отвергающий всё свыше 10 МБ, или партию выписок надо выдать на факсоподобное монохромное устройство, которое рисует каждый серый пиксель либо бумагой, либо тонером. Обе задачи — про ресемплинг, и у обеих есть быстрый ответ, который выглядит плохо, и медленный, который выглядит правильно
В чём три кернела реально различаются
У THPDFResampleKernel три значения, и они сидят в действительно разных точках кривой скорость-качество. rkHalftone делегирует историческому GDI-пути StretchBlt с режимом HALFTONE, который, несмотря на имя, является фильтрацией билинейного класса: быстрый, достаточный для line art и скриншотов и склонный к жёстким краям, которые вы мгновенно узнаёте на уменьшенных фотографиях. rkBicubic запускает сепарабельный кернел Catmull-Rom, а rkLanczos3 — сепарабельный windowed sinc с поддержкой в три лепестка
Оба сепарабельных кернела работают двумя проходами, горизонтальным затем вертикальным, с 6-12 тапами на целевой пиксель на чистом Pascal. Это примерно на порядок медленнее GDI-пути, чем и объясняется, почему rkHalftone остаётся дефолтом. На ночном батче из тысяч страниц разница — решение планировщика, а не предпочтение. На одном документе, которого ждёт пользователь, Lanczos3 почти бесплатен и заметно лучше
Два свойства реализации стоит знать, потому что они определяют, что вывод может и не может делать. Границы зажимаются репликацией края, а не заворачиванием или затуханием, и веса нормируются на каждый целевой пиксель. Вместе эти два свойства означают, что результат никогда не звенит ниже чёрного или выше белого, поэтому классический overshoot-ореол Lanczos вокруг жёсткого края не появляется как обрезанные артефакты в закодированном изображении
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // выставить до вызова
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
Аргумент MinimumSavingsBytes, выше это 4096, — охрана, держащая операцию честной. Перекодирование изображения, уже эффективно сжатого, может дать поток больше исходного, и даунсэмплер, слепо заменяющий каждое изображение, изредка вырастит файл, который его просили уменьшить. Порог говорит: подтверждай замену, только когда она экономит хотя бы столько байт. PreservedCalibratedImageCount репортит другое консервативное решение — изображения, оставленные нетронутыми, потому что несут калиброванное цветовое пространство, которое ресемплинг скомпрометировал бы
Почему неверный полиномиальный коэффициент так трудно заметить?
Потому что сломанный интерполяционный кернел не падает и не бросает исключений, он просто выдаёт изображение, выглядящее тонко неправильным так, что никто не может атрибутировать. Кернел Catmull-Rom кусочно-кубический, и его внешняя ветка в вложенной форме Хорнера — ((-0.5t + 2.5)t - 4)t + 2. Напишите тот средний коэффициент как -5 вместо -4, и функция всё ещё вычисляется, всё ещё возвращает числа в правдоподобном диапазоне и всё ещё производит изображение
Урон проявляется как W(1) = -1 там, где должно быть 0. Отрицательные веса накапливаются, сумма зажимается в ноль, и видимый симптом — градиент, чей левый конец уходит в чёрный, и ступенчатый край, потерявший промежуточные тона. Ничто в отказе не указывает на полином. Проверка, ловящая это за секунды, — арифметическая, а не визуальная: интерполирующий кернел обязан удовлетворять W(0) = 1 и W(±1) = W(±2) = 0, и любой кернел, промахнувшийся мимо этих трёх точек, содержит ошибку в коэффициенте, точка. Проверяйте эти три значения в юнит-тесте, и весь класс опечаточных дефектов исчезает
Дизеринг Floyd-Steinberg и его место в пайплайне
Проход дизеринга — задача иная, чем ресемплинг, и живёт в другой точке пайплайна. RenderOutputDither применяет диффузию ошибки Floyd-Steinberg после компоновки страницы, и это единственное размещение, имеющее смысл для монохромного превью печати или факсоподобного экспорта: операция о том, чтобы свести готовый растр к одному биту на пиксель, а не о том, как отдельные изображения масштабировались на входе
Сам алгоритм короток. Яркость порогуется на 50 процентах, и ошибка квантования диффундируется четырём соседям с классическими весами 7/16, 3/16, 5/16 и 1/16 — вправо, вниз-влево, вниз и вниз-вправо. Выходной пиксель — 0 или 255 в каждом канале. Наивная альтернатива, жёсткий порог без диффузии, превращает фотографию в силуэт и теряет каждый полутон, нёсший содержание
// Дизеринг при рендере для монохромного превью-устройства
Pdf.RenderOutputDither := True;
// Или примените тот же проход к уже имеющемуся битмапу. Битмап обязан
// быть pf24bit; функция возвращает False, а не угадывает
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Прямой доступ к кернелу, когда ресемплируете вне документного пайплайна
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
В диффузии ошибки есть одна деталь реализации, которая кусает каждого один раз. Буфер ошибок от строки к строке обязан накапливать. Каждый пиксель следующей строки получает вклад от трёх разных пикселей текущей — тапы 3/16, 5/16 и 1/16, — и если код присваивает вместо того, чтобы прибавлять, каждая запись отбрасывает предыдущий вклад, и выживает только последний тап. Изображение всё ещё выглядит дизеренным, чем и опасно — трудно заметить, но текстура неправильная, и тональная воспроизводимость уплывает. Тест, ловящий это, количественный: дизерьте равномерное поле серого середины и требуйте, чтобы внутреннее покрытие легло между 40 и 60 процентами
Какую комбинацию выбрать пайплайну уменьшения размера?
Подбирайте кернел под то, чем изображения являются на самом деле, а дизеринг считайте заботой устройства, а не сжатия. Для фотографических сканов, которым надо влезть в бюджет размера, rkLanczos3 на 150 или 200 DPI сохраняет детали, которые люди замечают, урезая число пикселей в четыре раза и больше. Для скриншотов, диаграмм и line art rkHalftone действительно хорош и куда быстрее, потому что в таких изображениях мало тональных градиентов для сохранения. Для смешанного батча, где каждое изображение не осмотришь, rkBicubic — разумная середина: лучше билинейного, примерно вдвое меньше тапов, чем у Lanczos3
Даунсэмплинг — один рычаг из нескольких, и не всегда самый большой. Двухуровневые сканы обычно куда лучше отвечают кодировщику из нативного JBIG2-сжатия bilevel в Delphi, где выигрыш идёт от словарей символов, а не от числа пикселей. Прежде чем решать, полезно знать, что в файле реально лежит, — для этого существует извлечение изображений и их decode-фильтров: инвентаризация объектов-изображений и их текущего сжатия говорит, есть ли у ресемплинга что выигрывать
Если вы строите поверхность превью, показывающую результат, тот же рендер-путь, задокументированный в рендере страницы PDF в битмап, — то место, где действует RenderOutputDither, так что дизеренное превью и дизерный вывод идут из одного пути кода, а не из двух реализаций, разбегающихся со временем
Широкий принцип за обеими возможностями: настройки качества должны быть явными и обратимыми. HotPDF держит историческое поведение дефолтом, чтобы существующее приложение обновилось без сюрприза в выводе или таймингах, и кладёт более красивые, более медленные пути на расстоянии одного присваивания свойства. Оба входят в Delphi PDF-компонент HotPDF рядом с машинерией оптимизации ресурсов и рендеринга, на которой они построены