Технічна стаття

Рендеринг сторінок PDF у зображення JPEG у Delphi за допомогою PDFium Component

Рендеринг сторінки PDF у формат JPEG — це дві операції, які зазвичай виконують разом, а потім налагоджують окремо. Спочатку ви растеризуєте сторінку в піксельне растрове зображення з вибраною роздільною здатністю. Потім ви передаєте це растрове зображення кодувальнику JPEG і вибираєте якість. PDFium Component бере на себе першу половину через 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
Мініатюра для списку7260-70
Екранний попередній перегляд96-15080-85
Високодеталізований перегляд200-30085-95
Майстер-копія для друку300-60090-100

Якість JPEG заслуговує на окреме застереження. Це не лінійна шкала. Стрибок від 70 до 85 дає реальне візуальне покращення при незначному збільшенні розміру файлу; стрибок від 95 до 100 приблизно подвоює файл заради різниці, яку майже ніхто не бачить, оскільки якість 100 все ще не є кодуванням без втрат, вона просто перестає відкидати багато даних. Для сторінок із великою кількістю тексту блокове стиснення JPEG розмазує чіткі краї гліфів у слабкий ореол, тому якість нижче приблизно 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 Component має шлях прогресивного рендерингу з маркером скасування; це більш важкий механізм, ніж потрібно для більшості пакетних експортів, але він доступний, коли рендеринг однієї сторінки при 600 DPI сам по собі достатньо повільний, щоб викликати блокування

І остання комбінація, про яку варто знати. Растеризація сторінки відкидає її текстовий шар: JPEG — це пікселі, і слова в ньому більше не можна виділяти чи шукати. Коли вам потрібне і зображення, і текст під ним, рендеріть зображення та витягуйте текст окремо, що розглядається у супровідній статті про витягнення тексту з документів PDF за допомогою PDFium Component. Перевантаження RenderPage та параметри рендерингу, показані тут, є частиною PDFium Component для Delphi та C++Builder