Ни fax gateway, ни архивный pipeline, который хранит миллион счетов со сканоподобным видом, ни OCR front-end, который порогово переводит все в черно-белое еще до распознавания символов, не хотят ваш 24-битный render страницы. Всем троим нужно одно и то же: чистый 1-битный bitmap, один bit на пиксель, где каждая точка - либо чернила, либо бумага. Дайте им полноцветный BMP, и они все равно выбросят 23 бита на пиксель, как правило, с худшим dithering pass, чем вы могли бы сделать сами. Интересный вопрос здесь в том, где именно должно происходить это down-conversion, и ответ у PDFlibPas говорит кое-что полезное о том, как расширять renderer, который вы совершенно не хотите переписывать
PDFlibPas - нативная Object Pascal PDF-библиотека для Delphi и C++Builder. Ее rendering core растеризует страницу в bitmap и умеет выдавать BMP, PNG, JPEG, WMF и еще несколько форматов. Чего у нее до недавнего времени не было, так это настоящего monochrome bitmap на выходе и рендера только части страницы. Оба режима появились в v3.83.0 и оба были построены как тонкие convenience layer поверх существующего renderer, а не как изменение самого rasterizer. Именно это ограничение и является всей историей
Почему down-convert нужно после rendering, а не внутри renderer
Очевидный способ получить 1-битное изображение - заставить rasterizer рисовать сразу в 1-bit. И это же самый короткий путь все сломать. Внутренний bitmap renderer создается с жестко заданным PixelFormat := pf24bit в конструкторе PDFlibRenderer , и эта 24-битная поверхность используется всеми render path: PNG export, preview через device context, JPEG output, всем подряд. Переключите ее на pf1bit в источнике, и вы не добавите monochrome feature, а ухудшите fidelity цвета для каждого вызывающего кода в библиотеке и подпишетесь под отладкой целого ряда downstream regression
Поэтому RenderPageToMonochromeFile идет от обратного. Сначала он рендерит страницу нормально, во временный 24-bit BMP, и только потом схлопывает его в 1-bit как post-processing step. Сам renderer не трогается. Весь monochrome behavior живет внутри convenience method, а значит, никак не затрагивает тех, кто его не вызывает. Такой компромисс стоит назвать прямо: post-process стоит одного дополнительного выделения bitmap и temp file, а взамен держит несущую core-часть полностью вне области риска. Для функции, которая нужна fax и archival edge case, это правильная сторона баланса
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// 200 DPI is the classic Group 4 fax resolution; page index is 1-based
Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
finally
Pdf.Free;
end;
end;
Как именно происходит схлопывание в 1-bit
Down-conversion опирается на GDI, а не на самописный threshold loop, и этот выбор важен для качества output. Внутри метода временный 24-битный bitmap загружается в TBitmap , затем создается второй TBitmap с PixelFormat := pf1bit и теми же размерами, а пиксели переносятся одним blit:
// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');
Фокус тут в SetStretchBltMode с HALFTONE . Несмотря на то что source и destination одинакового размера и масштабирования как будто не происходит, режим stretch все равно определяет, как GDI отображает цвета в 1-bit palette. HALFTONE заставляет его применять halftone dithering, превращая серые области и сглаженные кромки текста в узоры из черных и белых точек, а не в жесткую обрезку к ближайшему из двух цветов. Если убрать этот вызов или оставить default BLACKONWHITE , содержимое в градациях серого постеризуется в грубые пороговые формы. Для scanned-document и OCR-preprocessing output dithering почти всегда дает именно тот результат, который нужен
Одна деталь здесь абсолютно не обсуждается: временный render обязан быть BMP. RenderPageToMonochromeFile вызывает общий renderer с options code 0 , а это BMP. Аргумент options у RenderPageToFile - это небольшой integer enum, и значения в нем не взаимозаменяемы: 0 - BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG и так далее. Затем down-converter делает TBitmap.LoadFromStream на temp file. Если вместо BMP подать WMF, передав 2 , загрузка рухнет с "Bitmap image is not valid" , потому что Windows Metafile - это vector record stream, а не DIB. Monochrome down-conversion является raster operation от начала до конца, поэтому промежуточный формат тоже обязан быть raster
Рендер только подрегиона страницы
Второй метод, RenderPageRegionToFile , рендерит не всю страницу, а только ее прямоугольный фрагмент. Сценарии здесь легко узнать каждому, кто когда-либо делал document viewer: вырезать блок с подписью из договора, сгенерировать tile для увеличенной карты большого чертежа или вытащить один штампованный регион в thumbnail, не оплачивая rasterize всей страницы на высоком DPI. Сигнатура здесь вполне прямолинейна:
// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');
Строка clip представляет собой четыре double в PDF points, разделенные запятыми, и вручную парсится внутри метода, чтобы обойти locale и quirks DelimitedText . По width и height метод вычисляет размер выходного bitmap как Round(Width * DPI / 72) на Round(Height * DPI / 72) , выделяет in-memory bitmap pf24bit ровно такого размера и рендерит в его device context через RenderPageToDCClip . В результирующий файл попадает только clipped rectangle, и его размер соответствует именно региону, а не всей странице
Параметр clip, который ничего не делал
Именно здесь работа оказалась острее, чем кажется. RenderPageToDCClip уже давно нес Clip как параметр, но это была ложь. Вызов принимал аргумент, передавал его вниз в TPDFPageTree.RenderPageToDC , а та реализация полностью его игнорировала и вообще не передавала renderer. Можно было передать любой rectangle и все равно получить назад полную страницу. Любой, кто уже завел у себя RenderPageToDCClip в расчете на crop, в действительности получал full-page render и, в зависимости от layout, мог этого даже не заметить
В v3.83.0 провод наконец подключили. RenderPageToDC теперь парсит тот же rectangle "Left,Top,Width,Height" в points и применяет его как реальную GDI clip region к target device context перед render. Преобразование из points в device pixels идет по обычному scale factor DPI / 72 и применяется ко всем четырем краям. Последовательность вокруг render использует стандартный танец save/clip/restore:
// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
Round(ClipLeft * ScaleFactor),
Round(ClipTop * ScaleFactor),
Round((ClipLeft + ClipWidth) * ScaleFactor),
Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);
Пара SaveDC / RestoreDC(-1) и делает вызов безопасным при многократном использовании: clip region кладется в стек состояния DC, затем страница рисуется, а исходный clip возвращается независимо от того, как именно завершился render. RestoreDC(TargetDC, -1) восстанавливает последнее сохраненное состояние, и это стандартная idiom для balanced save/restore. Пропустите restore, и вызывающий код, который использует тот же DC для следующего full-page render, внезапно обнаружит, что он mysteriously clipped к последнему региону. Исправление мертвого параметра автоматически починило и RenderPageRegionToFile , потому что новый метод идет ровно через тот же path
Один behavioral момент важно усвоить: clip обрезает , а не масштабирует . Страница по-прежнему растеризуется при запрошенном DPI и в своем обычном положении, а clip region просто выбрасывает все, что лежит вне rectangle. Вы не растягиваете регион так, чтобы он заполнил output; вы вырезаете окно из full-resolution render. Если нужен увеличенный регион, поднимайте DPI. Координаты rectangle интерпретируются в device space после scaling из points в pixels и измеряются от верхнего левого угла render surface, поэтому планируйте Left и Top сверху страницы вниз. Более глубокий разбор того, как PDFlibPas управляет device context при on-screen output, есть в сопутствующем материале о print preview and device-context output , который проходит по той же DC plumbing уже со стороны display
Честная граница: 1-bit BMP, а не G4 TIFF
Эту возможность легко было бы рекламировать как "fax-ready output", поэтому границу лучше проговорить прямо. RenderPageToMonochromeFile производит pf1bit 1-bit BMP. Он не производит CCITT Group 4 TIFF, который как раз и ожидает настоящий fax workflow или TIFF archive. Причина здесь не в забывчивости, а в конкретном техническом факте: unit CCITT у PDFlibPas сейчас умеет декодировать G4 stream, но не содержит G4 encoder . Без encoder просто некуда записывать сжатые monochrome run, поэтому monochrome path заканчивается на несжатом 1-bit DIB
На практике это все равно полезно. 1-bit BMP - это правильный pixel format, уже dithered и готовый, а большинство fax, archival или OCR toolchain спокойно его проглотят либо сами преобразуют в G4 еще одним downstream step. Но если требование звучит буквально как "Group 4 TIFF прямо из библиотеки", то это пока не тот случай, и вам следует планировать собственную стадию compression. Знать, где именно feature останавливается, ничуть не менее важно, чем знать, что она делает
Оба метода намеренно маленькие, и это и есть главный design lesson со всей страницы: convenience API, сидящий поверх renderer, может добавить реальную возможность, monochrome output и region crop, не лезя внутрь rasterizer и не дестабилизируя всех остальных caller. А если вам нужно выбирать между несколькими rendering engine под самим rasterization, подробный обзор multi-engine PDF rendering in Delphi разбирает все компромиссы подробно. Чтобы увидеть полный rendering surface и остальной API, смотрите PDFlibPas Delphi PDF Library - на странице продукта собрана полная картина