Отрисовка страницы PDF в JPEG — это две операции, которые люди склонны выполнять вместе, а затем отлаживать по отдельности. Сначала вы растеризуете страницу в растровое изображение в пикселях с выбранным вами разрешением. Затем вы передаете это растровое изображение в кодировщик JPEG и выбираете качество. Компонент PDFium отвечает за первую половину с помощью RenderPage; вторая половина — это простой VCL, TJPEGImage из Vcl.Imaging.jpeg. Именно на их стыке принимаются интересные решения, потому что разрешение, которое вы выбираете на стороне рендеринга, и качество, которое вы выбираете на стороне кодирования, компенсируют друг друга и размер файла таким образом, что легко ошибиться
Что нужно усвоить перед написанием любого кода: страница PDF не имеет пикселей. Она описывается в пунктах, где один пункт равен 1/72 дюйма, а страница представляет собой векторный рисунок, измеряемый в этих пунктах. Когда вы просите PDFium выполнить рендеринг, вы выбираете, на какое количество пикселей проецировать этот рисунок, и этот выбор — это DPI. Если вы ошибетесь в арифметике, вы либо получите размытую миниатюру вместо печатного оригинала, либо выделите 200-мегапиксельное растровое изображение для того, что должно быть превью размером 120 пикселей
От DPI к размерам в пикселях
RenderPage требует целочисленных значений Width и Height в пикселях, а не DPI. Поэтому первая задача — это преобразование. Страница сообщает свой размер в пунктах через PageWidth и PageHeight (оба Double), и преобразование такое же, как и в любом растеризаторе: пиксели равны пунктам, умноженным на целевой DPI и разделенным на 72. Страница формата US Letter имеет размер 612 на 792 пункта. При 150 DPI это превращается в 1275 на 1650 пикселей; при 72 DPI она остается 612 на 792, один пиксель на пункт, что, как люди часто забывают, является просто идентичностью
// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free; // the function-form RenderPage hands you ownership
Две детали в этих четырех строках определяют, правилен ли код. Во-первых, функциональная форма RenderPage возвращает TBitmap, которым вы владеете. PDFium выделил его и отошел в сторону; если вы не вызываете Free для него на каждой итерации, пакет из нескольких сотен страниц приведет к утечке нескольких сотен растровых изображений, и процесс будет раздуваться, пока что-нибудь не упадет. Во-вторых, аргумент Color, здесь это clWhite. Страницы PDF обычно рисуются в расчете на непрозрачную белую подложку, и страница с прозрачностью, отрендеренная на неправильном цвете фона, дает грязные края или случайные темные ореолы. Белый — правильный цвет по умолчанию почти для каждого документа; этот параметр существует для тех редких случаев, когда это не так
Значения 0, 0 — это смещения Left и Top на странице в масштабированном пространстве координат, и вы оставляете их равными нулю, если только не выполняете обрезку. ro0 — это поворот: оставьте его равным нулю, и PDFium учтет любой поворот, который страница уже объявляет в своей записи /Rotate, поэтому страница, созданная в альбомной ориентации, выходит альбомной без каких-либо действий с вашей стороны
Кодирование растрового изображения в JPEG
Как только растровое изображение существует, JPEG становится простой частью, и это чистый Delphi. TJPEGImage.Assign копирует растровое изображение, CompressionQuality устанавливает качество по шкале от 1 до 100, а SaveToFile записывает файл. Единственное правило порядка заключается в том, что качество должно быть установлено до сохранения, поскольку оно управляет кодированием, которое запускает SaveToFile
uses
Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;
procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
const FileName: string);
var
Bitmap: TBitmap;
Jpeg: TJPEGImage;
begin
Pdf.PageNumber := PageNumber;
Bitmap := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Dpi / 72),
Round(Pdf.PageHeight * Dpi / 72),
ro0, [], clWhite);
try
Jpeg := TJPEGImage.Create;
try
Jpeg.Assign(Bitmap);
Jpeg.CompressionQuality := Quality; // 1..100
Jpeg.SaveToFile(FileName);
finally
Jpeg.Free;
end;
finally
Bitmap.Free;
end;
end;
Этот вложенный try/finally выглядит суетливо для одностраничного помощника, но он идеально подходит для пакета. Внутренний блок освобождает кодировщик, внешний блок освобождает растровое изображение, и срабатывание любого из них при исключении по-прежнему освобождает то, чем он владеет. Сверните их в один, и исключение во время кодирования может привести к зависанию растрового изображения. В долгосрочной перспективе это разница между конвертером, который завершает работу, и тем, который умирает на 300-й странице с поврежденным файлом и диалоговым окном нехватки памяти
Совместный выбор DPI и качества
Два регулятора не зависят от цели вывода, и распространенной ошибкой является выкручивание обоих на максимум из соображений предосторожности. Веб-миниатюра, отрисованная с разрешением 300 DPI и сохраненная с качеством 95, представляет собой несколько сотен килобайт, притворяющихся 120-пиксельным изображением; браузер отбрасывает почти все это при уменьшении масштаба. Подберите разрешение в соответствии с количеством пикселей, которые действительно требуются для вывода, а затем выберите качество, которое выдержит сжатие JPEG с потерями без видимых артефактов
| Вывод | DPI | Качество JPEG |
|---|---|---|
| Миниатюра для списка | 72 | 60-70 |
| Превью на экране | 96-150 | 80-85 |
| Просмотр с высокой детализацией | 200-300 | 85-95 |
| Оригинал для печати | 300-600 | 90-100 |
Качество JPEG заслуживает отдельного предостережения. Это не линейный регулятор. Прыжок с 70 до 85 дает реальное визуальное улучшение при скромном увеличении размера файла; прыжок с 95 до 100 примерно удваивает файл ради разницы, которую почти никто не замечает, потому что качество 100 все еще не является сжатием без потерь, оно просто перестает много отбрасывать. Для страниц с большим количеством текста блочное сжатие JPEG размазывает острые края глифов в слабый ореол (ringing), поэтому качество ниже 80 делает текст похожим на отсканированный на том выводе, который должен быть четким. Если страницы состоят в основном из текста и вы можете изменить формат, PNG отрисовывает этот текст без ореолов; JPEG занимает свое место на фотографическом и смешанном контенте, где его сжатие действительно меньше
Более быстрые и маленькие миниатюры
Когда целью является миниатюра, а не точное воспроизведение, вы можете дать указание средству рендеринга выполнять меньше работы. Параметр Options принимает набор флагов TRenderOption, и некоторые из них жертвуют точностью ради скорости именно так, как этого требует небольшое превью. reGrayscale отбрасывает цвет, что ускоряет рендеринг и создает растровое изображение меньшего размера для кодирования. reNoSmoothImage и reNoSmoothPath пропускают сглаживание, которое все равно невидимо в масштабе миниатюры
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
Scale: Double;
begin
Pdf.PageNumber := PageNumber;
// Fit the page inside MaxW x MaxH while preserving aspect ratio.
Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
Result := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Scale),
Round(Pdf.PageHeight * Scale),
ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;
Случай с миниатюрой также показывает более понятный способ думать о размере. Вместо того, чтобы использовать DPI, вычислите единый масштабный коэффициент, который умещает страницу внутри ограничивающего прямоугольника и сохраняет соотношение сторон, что и делает функция Min из двух соотношений. Страница с книжной ориентацией и страница с альбомной ориентацией оказываются внутри одного и того же прямоугольника без искажений, и вам никогда не придется рассуждать о том, какой DPI соответствует «вписать в 200 на 280». Одно предостережение относительно reGrayscale: этот флаг преобразует содержимое растрового изображения в оттенки серого, но векторные заливки и текст сохраняют свои цветовые значения в движке, поэтому страница, состоящая в основном из векторной графики, может оказаться не такой монохромной, как предполагает название флага. Для получения действительно полного результата в оттенках серого надежным путем является преобразование отрендеренного растрового изображения с помощью GrayscalePdfBitmap
Пакетная обработка всего документа
Сборка этого для всего документа представляет собой цикл по PageCount, при котором PageNumber перемещается по одной странице за раз. Страницы нумеруются с 1: первая страница — это PageNumber := 1, и цикл выполняется до PageCount включительно, а не до PageCount - 1. Еще одна вещь, которую пакетный процесс должен соблюдать, — это контракт тихой загрузки. Установка Active := True никогда не вызывает исключения при поврежденном файле или неверном пароле; она просто оставляет Active равным False. Проверьте это перед рендерингом первой страницы, иначе первый вызов RenderPage будет работать с документом, который так и не был открыт
procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
Pdf: TPdf;
I, Digits: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := PdfPath;
Pdf.Active := True;
if not Pdf.Active then
raise Exception.Create('Could not open ' + PdfPath);
Digits := Length(IntToStr(Pdf.PageCount)); // zero-pad so files sort right
for I := 1 to Pdf.PageCount do
SavePageAsJpeg(Pdf, I, Dpi, Quality,
Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Добавление нулей слева с помощью Digits — это мелочь, которая впоследствии сэкономит вам полдня. Назовите файлы от page_1.jpg до page_10.jpg, и любой инструмент, который сортирует их как строки, поместит page_10 сразу после page_1, перепутав порядок. Дополнение нулями до ширины самого большого номера страницы, так что 300-страничный документ дает page_001.jpg, сохраняет лексический порядок и порядок страниц одинаковыми везде на последующих этапах
Для документов, которые достаточно велики, чтобы преобразование занимало заметное время, запустите его вне потока пользовательского интерфейса или прокачивайте сообщения между страницами, чтобы приложение оставалось отзывчивым, и дайте пользователю возможность остановить процесс. Если вы выполняете рендеринг очень больших страниц и хотите, чтобы отмена срабатывала в середине страницы, а не только между страницами, компонент PDFium имеет путь прогрессивного рендеринга с токеном отмены; это более тяжелый механизм, чем требуется для большинства видов пакетного экспорта, но он доступен, когда одна страница при 600 DPI отрисовывается достаточно медленно, чтобы заблокировать работу
Еще одно сочетание, которое стоит знать. При растеризации страницы ее текстовый слой отбрасывается: JPEG — это пиксели, и слова в нем больше нельзя выделять или искать. Когда вам нужно и изображение, и лежащий в его основе текст, выполните рендеринг для изображения и извлеките текст отдельно, о чем рассказывается в сопутствующей статье по извлечению текста из PDF-документов с помощью компонента PDFium. Перегрузки RenderPage и параметры рендеринга, показанные здесь, являются частью компонента PDFium для Delphi и C++Builder