losLab PDF Library for Delphi сжимает отсканированную страницу через Mixed Raster Content методом TPDFlib.CompressPageAsMRC. Метод рендерит страницу, пороговой обработкой выделяет полноразрешённую 1-битную маску переднего плана, уживает остальное в JPEG-фон и собирает страницу заново из двух изображений: фона /DCTDecode и трафаретной маски /CCITTFaxDecode, закрашенной текущим цветом заливки
Страница, ради которой это существует, — та самая, которой забит любой документный архив: цветной скан счёта или договора в 300 DPI, сохранённый одним большим JPEG. Закодируйте с качеством, при котором текст остаётся читаемым, — файл получится большим; сбавьте качество — и буквы обрастут звенящими ореолами и размазанными засечками, потому что резкая чёрная кромка на белой бумаге — то, что JPEG делает хуже всего. Mixed Raster Content перестаёт просить один кодек делать две работы
Почему одиночный JPEG делает отсканированный текст одновременно мыльным и тяжёлым?
Одиночный JPEG плохо справляется с отсканированным текстом, потому что DCT-кодек тратит биты на плавные градиенты, а жёсткие кромки аппроксимирует высокочастотными коэффициентами, которые квантование выбрасывает первыми. Текст на скане — почти сплошные жёсткие кромки. Держать эти кромки чистыми заставляет высокое качество по всей странице, включая фактуру бумаги, логотип и кофейное пятно, которым оно было не нужно
Идея MRC делит страницу по тому, чем каждая область является на самом деле. Чернила становятся билевел-изображением, где факсовый кодек вроде CCITT без потерь и очень компактно записывает серии чёрных и белых пикселей. Всё остальное — тон бумаги, фотографии, цветные подложки — становится непрерывнотоновым изображением, которое можно хранить в малой доле разрешения, потому что фон никто не читает в 300 DPI. Билевел-слой держит текст чётким в полном разрешении, фоновый слой хранит цвет дёшево
Как CompressPageAsMRC делит страницу на два слоя?
TPDFlib.CompressPageAsMRC гонит один фиксированный конвейер на страницу. Он рендерит страницу в запрошенном DPI в 24-битный bitmap, классифицирует каждый пиксель по взвешенной яркости Rec. 601 (299/587/114 промилле) против Threshold и пишет результат в 1-битную маску того же разрешения. Параллельно он уживает тот же рендер по BGScale усреднением блоками, так что BGScale 4 превращает каждый блок 4×4 пикселя в один фоновый пиксель
Затем содержимое страницы заменяется. Исходные операторы удаляются, ужатый фон встраивается как изображение /DCTDecode в /DeviceRGB (ISO 32000-1 §7.4.8), маска кодируется библиотечным CCITT-энкодером как изображение /CCITTFaxDecode (§7.4.6) и помечается /ImageMask true, и оба изображения рисуются на весь page box, маска поверх. Сигнатура короткая:
function CompressPageAsMRC(Page: Integer; DPI: Double;
BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
Pageнумеруется с единицы и обязан лежать в пределахPageCountDPI— разрешение рендера исходного растра, от 50 до 1200BGScale— коэффициент ужатия фона, от 1 до 8JPEGQuality— качество фонового JPEG, от 1 до 100Threshold— порог яркости, от 1 до 255; пиксели на нём и выше — бумага, более тёмные — чернилаOptionsзарезервирован и должен быть 0
Типичный прогон по отсканированному файлу выглядит так. Вызывайте SetFillColor прямо перед каждой страницей — причина объясняется в следующем разделе
uses
SysUtils, PDFlibrary;
procedure CompressScannedFile(const SrcFile, DstFile: string);
var
Lib: TPDFlib;
P, Replaced: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SrcFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0); // цвет чернил трафаретного слоя
// маска 300 DPI, фон 75 DPI, качество JPEG 60, порог по яркости 160
Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
if Replaced = 0 then
raise Exception.CreateFmt('MRC failed on page %d', [P]);
end;
if Lib.SaveToFile(DstFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [DstFile]);
finally
Lib.Free;
end;
end;
Почему передний план закрашивается текущим цветом заливки?
Слой переднего плана — трафаретная маска, а у трафаретной маски нет собственного цвета. ISO 32000-1 §8.9.6.2 определяет изображение с /ImageMask true как 1-битную форму: значения сэмплов помечают, куда пойдёт краска, а краска — это текущий nonstroking color из графического состояния. Именно это делает слой дешёвым, и по той же причине CompressPageAsMRC не принимает параметра цвета
TPDFlib.CompressPageAsMRC красит маску цветом, выставленным SetFillColor, поэтому порядок вызовов важен. Рисовальщик выпускает цвет заливки лениво: SetFillColor запоминает цвет и помечает его изменённым, а отрисовка трафарета пишет оператор rg внутри собственного блока q/Q, только пока это изменение ещё не погашено. Нарисуйте между делом заполненную фигуру — и отложенное изменение уйдёт на неё. Выставляйте цвет прямо перед каждой страницей, как в пакете выше, и гадать не придётся. Обычный выбор — чёрный; тёмно-синий хорош для форм, заполненных синими чернилами, когда хочется, чтобы вывод меньше походил на ксерокопию
// Синяя шариковая ручка: рисуем слой чернил тёмно-синим
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);
Размещение следует за началом координат библиотеки. CompressPageAsMRC читает GetOrigin и рисует оба изображения от верхнего края page box: верхняя координата 0 при начале сверху-слева (SetOrigin(1)), верхняя координата PageHeight при стандартном начале снизу-слева. Подстраивать ничего не нужно, но это объясняет, почему слои ложатся правильно при любом начале координат, которое уже использует ваш конвейер
Одно следствие семантики трафарета стоит сказать прямо: каждый пиксель, классифицированный как чернила, получает один и тот же цвет. Красный штамп, синяя подпись и чёрный основной текст все оказываются одним цветом заливки на слое переднего плана. Слабый низкоразрешённый след исходных цветов хранит только фоновый JPEG под ними
Выбор DPI, BGScale, Threshold и качества JPEG
Значение DPI определяет резкость текста, и 300 — разумное значение по умолчанию для офисных сканов. Маска хранится в полном разрешении рендера, так что DPI задаёт, штрих какой тонкости переживёт порог. Подниматься выше разрешения исходного скана смысла нет: рендерер будет лишь растягивать встроенное изображение скана. Второй предел — память: страница US Letter в 300 DPI разворачивается в 2550×3300 пикселей, 25 245 000 байт 24-битных пиксельных данных до вывода маски и фона, а та же страница на потолке 1200 DPI — это 10200×13200 пикселей, около 404 МБ
BGScale разменивает детализацию фона на размер. При 300 DPI коэффициент 4 даёт фон 75 DPI, 638×825 пикселей для Letter — более чем достаточно для тона бумаги и плоских цветовых областей. Страницам с фотографиями или растровыми иллюстрациями, на которые читатель будет реально смотреть, заслуживают коэффициент 2; простому тексту на белой бумаге подойдёт 6 или 8, потому что фон там едва ли не просто оттенок
Threshold — параметр, который меняет облик страницы. Повышение относит к чернилам больше серых полутонов: тонкие штрихи и карандашные пометки толстеют, но вместе с ними втягиваются шум сканера и тень бумаги в виде крапинок. Понижение даёт более чистую бумагу и более тонкие, порой рвущиеся буквы. Значения около 128–170 — разумный стартовый диапазон для чёрного текста на белом; компромисс тот же, что обсуждается в статье о рендере PDF-страниц в 1-битный монохром в Delphi, где тот же порог яркости рулит RenderPageToMonochromeFileEx. В отличие от того метода, у CompressPageAsMRC нет прохода чистки крапинок, поэтому шумным сканам лучше подойдёт чуть меньший порог
JPEGQuality значит меньше, чем для одиночного изображения скана, потому что фон больше не несёт кромок текста. Качество 50–70 обычно незаметно на фоне 75 DPI позади резкой маски
Что говорит возвращаемое значение, а чего оно не говорит?
TPDFlib.CompressPageAsMRC возвращает размер в байтах заменённого содержимого страницы и 0 при сбое. Это индикатор успеха, а не мера экономии. На типичной отсканированной странице исходный поток содержимого — несколько десятков байтов, ровно столько операторов, чтобы уложить одно изображение, так что малое положительное число — нормальный результат, даже когда страница худеет на мегабайты. Страница с изначально пустым потоком содержимого вернёт 0, хотя была пересобрана
Поведение при сбое не атомарно, и именно вокруг этого стоит проектировать. Проверки аргументов, рендер и кодирование JPEG идут до того, как страница тронута, поэтому BGScale вне диапазона, JPEGQuality 101 или сбой рендера оставляют страницу ровно как была. Но после очистки исходного содержимого неудача при встраивании одного из слоёв или его отрисовке тоже вернёт 0, и к этому моменту страница может быть уже частично пересобрана. Считайте любой 0 поводом выбросить документ из памяти, а не сохранять его, и никогда не пишите результат поверх исходного файла
Чтобы узнать, что конверсия реально сэкономила, измерьте документ. Быстрейшая проверка — сравнить длину SaveToString до и после; для ответа по категориям аудит размера PDF с AuditDocumentSpace покажет, ушла ли масса из изображений, как ожидалось
function MRCSavings(Lib: TPDFlib): Int64;
var
Before: Int64;
P: Integer;
begin
Before := Length(Lib.SaveToString);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0);
if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
end;
Result := Before - Length(Lib.SaveToString);
end;
Где этот двухслойный MRC отстаёт от ITU-T T.44
CompressPageAsMRC реализует нарочно упрощённый MRC. Полная модель ITU-T T.44 использует три слоя на полосу: фоновое изображение, изображение переднего плана, несущее цвета чернил, и билевел-маску, выбирающую между ними попиксельно, с местом для нескольких пар маска-передний план на одной странице. На языке PDF третий слой был бы низкоразрешённым цветным изображением переднего плана с явной маской (§8.9.6.3) вместо трафарета, закрашенного одним цветом. PDF Library for Delphi пропускает цветной слой переднего плана — именно поэтому результат так мал и именно поэтому цветные чернила схлопываются в цвет заливки
- Метод подходит для чёрного или одноцветного текста на отсканированной бумаге; документы с цветовой маркировкой, выделенный текст и цветные штампы теряют цвет чернил
- Пересобранная страница — чистый растр: любой настоящий текст, векторная графика и выделяемые символы исходной страницы становятся пикселями, поэтому гоняйте его на сканах, а не на born-digital страницах, где правильные инструменты — сабсеттинг шрифтов и ужатие изображений
- Маска использует библиотечный CCITT-энкодер в двухмерном режиме T.4, записанный как
/CCITTFaxDecodeс/K 1; у энкодера есть и режим Group 4 черезAddImageCCITTFromBitmapStream, ноCompressPageAsMRCего не выставляет, и JBIG2 он не использует - Аннотации и поля форм не сплющиваются; они остаются объектами поверх пересобранного содержимого
- Фон —
/DeviceRGB, поэтому вывод не годится для конвейеров PDF/X-1a, а самSetFillColorв режиме PDF/X-1a отклоняется - Скан, которому и так нужна OCR, стоит распознать до MRC, пока исходные пиксели ещё существуют в полной цветовой глубине
FAQ
Сохраняет ли CompressPageAsMRC текст искомым?
Нет. CompressPageAsMRC заменяет содержимое страницы двумя изображениями, поэтому любые текстовые операторы исходной страницы, включая невидимый OCR-слой, удаляются вместе с ним. Прогоните OCR по сжатой странице после, либо добавьте текстовый слой обратно уже после сжатия
Можно сжать только часть страниц документа?
Да. CompressPageAsMRC обрабатывает за вызов одну страницу с нумерацией с единицы, так что страницы выбираете вы и можете варьировать BGScale и Threshold постранично: например, коэффициент 2 для фото-вкладки и 6 для страниц с простым текстом
Каким должен быть параметр Options?
Передавайте 0. Options зарезервирован на будущее в текущем выпуске и ни один бит не определён, поэтому ненулевое значение сегодня ничего не значит
CompressPageAsMRC вышел в losLab PDF Library for Delphi v3.561.0 вместе со строительными блоками, из которых собран, — рендером, JPEG, CCITT и трафаретными масками, — и все они доступны как публичные методы, если вам нужен вариант конвейера. Библиотека, поддержка C++Builder и полный справочник API — на странице продукта PDF Library for Delphi