PDF хранит изображения как объекты первого класса внутри своих потоков содержимого. Когда страница ссылается на фотографию, скан или диаграмму, пиксельные данные находятся в словаре XObject вместе с геометрией страницы. Компонент PDFium предоставляет доступ к этому через два свойства в TPdf: BitmapCount, которое возвращает количество встроенных изображений на текущей странице, и Bitmap[Index], которое декодирует одно из них в TBitmap, которым вы владеете и должны освободить. В этом и заключается вся модель извлечения. Цикл состоит из четырех строк; что требует рассудительности, так это окружающая обвязка
Открытие документа
Первое, что нужно знать о TPdf, это то, что Active := True никогда не вызывает исключений. Ошибки загрузки, неверные пароли, поврежденные файлы — все они проглатываются внутренне, и компонент просто остается неактивным. Вам нужно самостоятельно проверить флаг после присваивания, иначе вы перейдете к циклу по страницам, где PageCount вернет ноль, и будете удивляться, почему ничего не извлеклось
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
Writeln('Failed to open: ', Pdf.FileName);
Exit;
end;
Writeln(Pdf.PageCount, ' pages');
// proceed to extraction
finally
Pdf.Free;
end;
end;
Файлы, защищенные паролем, следуют тому же шаблону: назначьте Pdf.Password перед установкой Active := True. Если пароль неверен, Active остается False, и вы не получаете исключения для перехвата. В пакетной утилите, обрабатывающей сотни файлов, такое тихое поведение на самом деле полезно: вы накапливаете ошибки в списке вместо того, чтобы раскручивать стек вызовов для каждой из них
Перебор страниц и извлечение изображений
BitmapCount работает для каждой страницы, поэтому перед его чтением необходимо установить Pdf.PageNumber. Номера страниц начинаются с 1; по умолчанию — 0, что означает, что ни одна страница не загружена. Свойство Bitmap[Index] имеет индекс от 0 и возвращает принадлежащий вызывающей стороне TBitmap. Вы должны его освободить. Пренебрегите освобождением внутри длинного цикла по большому документу, и объем используемой памяти будет быстро расти, потому что каждое изображение может занимать несколько мегабайт сырых пиксельных данных до любого сжатия
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
Page, Idx: Integer;
Bmp: TBitmap;
OutPath: string;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
for Idx := 0 to Pdf.BitmapCount - 1 do
begin
Bmp := Pdf.Bitmap[Idx];
if not Assigned(Bmp) then
Continue;
try
OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
Bmp.SaveToFile(OutPath);
finally
Bmp.Free;
end;
end;
end;
end;
Проверка Assigned имеет значение. Небольшое количество PDF-генераторов записывают XObjects изображений с нулевыми размерами пикселей или другими искаженными данными; в этих случаях компонент возвращает nil вместо пустого изображения. Относиться к возврату nil как к ошибке и останавливать извлечение — неправильный рефлекс: пропустите это, запишите страницу и индекс в журнал, если вам нужен аудит, и продолжайте. Оставшаяся часть страницы все еще может дать валидные изображения
Обратите внимание, что внешний цикл устанавливает Pdf.PageNumber на каждой итерации. Именно это присваивание загружает страницу во внутреннее состояние компонента и делает BitmapCount значимым. Пропустите это, и вы будете повторно читать счетчик одной и той же страницы. Этот шаблон кажется избыточным, когда вы его пишете, но именно так устроен API: страница — это курсор, а не коллекция
Выбор формата вывода
BMP сохраняет данные без потерь и всегда доступен без дополнительных модулей, что делает его надежным вариантом по умолчанию, когда вы еще не знаете, что содержит изображение. Если важен размер файла, формат пикселей возвращаемого TBitmap подскажет вам, какой кодек подходит. 32-битное изображение несет альфа-канал; PNG сохраняет его без потерь. Большое 24-битное изображение с непрерывными тонами является кандидатом для JPEG. Небольшие изображения или те, что нарисованы с ограниченной палитрой, обычно лучше оставлять как BMP, чем прогонять через JPEG, который добавляет блочные артефакты при низких настройках качества и экономит мало при высоких
procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
Jpg: TJPEGImage;
begin
case UpperCase(ExtractFileExt(FileName)) of
'.JPG', '.JPEG':
begin
Jpg := TJPEGImage.Create;
try
Jpg.Assign(Bmp);
Jpg.CompressionQuality := 85;
Jpg.SaveToFile(FileName);
finally
Jpg.Free;
end;
end;
else
Bmp.SaveToFile(FileName); // BMP: lossless, no extra units
end;
end;
На практике выбор формата зависит от Bmp.PixelFormat и размеров. Если PixelFormat = pf32bit, вам нужен формат, поддерживающий альфа-канал; PNG является очевидным выбором, хотя он и требует модуля PNGImage в старых версиях Delphi. Для 24-битных изображений шире примерно 300 пикселей JPEG с качеством 85 дает трехкратное уменьшение размера по сравнению с BMP без заметных потерь для большинства фотографического контента. Ниже этого порога BMP сопоставим по размеру и позволяет полностью избежать принятия решения о качестве
Что считает и не считает BitmapCount
PDF различает XObjects изображений и векторную графику, нарисованную с помощью операторов путей. Страница, которая выглядит визуально сложной, может вернуть нулевое значение BitmapCount, если каждый элемент является векторным. Отсканированные страницы почти всегда возвращают ровно один элемент: сканер записывает весь скан как один полностраничный XObject изображения в том разрешении, на которое был настроен сканер. Страницы, в которых сверстанный текст смешивается со встроенными фотографиями, возвращают по одной записи на фотографию. Декоративные линии, заштрихованные фоны и границы таблиц обычно вообще не отображаются в количестве изображений
Счетчик также не включает встроенные изображения (inline images), редко используемую конструкцию PDF, в которой данные изображения встраиваются непосредственно в поток содержимого страницы, а не как именованный XObject. Они выходят за рамки того, что предоставляет этот API; в реальных документах они встречаются настолько редко, что большинство инструментов извлечения их просто не обрабатывают
Одна деталь, о которой стоит помнить: прочитанный вами BitmapCount относится к текущей странице на момент последнего присваивания PageNumber. Если ваш код разветвляется или вызывает какую-либо функцию, которая изменяет PageNumber между подсчетом и извлечением, вы можете прочитать меньше изображений, чем для них было выделено места, или обратиться за пределы индекса. Оставьте чтение счетчика и цикл Bitmap[] на одной странице, не изменяя PageNumber между ними
Использование TPdfView в приложении с формами
Компонент TPdfView предоставляет те же свойства BitmapCount и Bitmap[], но страница, с которой он читает данные, является текущей отображаемой страницей компонента просмотра, а не TPdf.PageNumber. Эти два указателя страниц независимы; установка одного не сдвигает другой. В VCL-приложении с формами и активным средством просмотра вы можете вызвать Pdf.PageNumber := N, чтобы управлять извлечением через TPdf, в то время как компонент просмотра останется на том месте, до которого пользователь докрутил в последний раз. Такое разделение сделано намеренно и сохраняет состояние отображения компонента просмотра чистым во время выполнения фонового извлечения
Память и производительность в пакетных заданиях
При работе с большим архивом главным, за чем нужно следить, является бюджет памяти. Каждый вызов Bitmap[] выделяет новый TBitmap в куче, и для отсканированной страницы в 300 DPI это легко может составить 25 МБ сырых пиксельных данных до какого-либо кодирования. Если вы обрабатываете страницы в плотном цикле без освобождения между итерациями, рабочий набор растет линейно с количеством изображений. Правильный подход всегда таков: получите одно изображение, сделайте то, что нужно, освободите его, получите следующее. Если вам нужно держать ссылки на несколько изображений одновременно для шага сравнения, сначала посчитайте их с помощью BitmapCount и выделите свой контейнер соответствующим образом, а затем освобождайте каждое сразу после завершения работы с ним, а не откладывайте очистку до конца документа. В документе с 500 отсканированными страницами это различие может означать разницу между 25 МБ и 12 ГБ максимального потребления памяти
Свойства BitmapCount и Bitmap[], показанные здесь, являются частью компонента PDFium Component для Delphi и C++Builder