Техническая статья

Объединение отсканированных изображений в один PDF с помощью компонента PDFium в Delphi

Группа обработки претензий пропускала бумажные файлы за тридцать лет через сканер с полистовой подачей. Сканер выдавал по одному JPEG-файлу на страницу в папку с именами 0001.jpg, 0002.jpg и так далее. На самом деле архиву нужен был один PDF-файл на каждое дело со страницами по порядку, чтобы рецензент мог открыть один документ вместо того, чтобы пролистывать сотню миниатюр изображений. Именно эта последняя задача — превращение пронумерованной стопки отсканированных изображений в один упорядоченный PDF — здесь и выполняется

Компонент PDFium справляется с этим напрямую. Помимо рендеринга и извлечения текста, компонент может создать PDF-файл с нуля: создать пустой документ, добавить пустую страницу любого нужного вам размера, поместить изображение на эту страницу в координатах пространства пользователя, а затем сохранить. Весь конвейер находится в компоненте TPdf, поэтому пакетный конвертер — это цикл по именам файлов плюс несколько вызовов

Форма преобразования

Для каждого сканирования должны произойти три вещи. Вы определяете размер страницы, размещаете изображение на странице, оставляя поле, и переходите к следующей странице. Компонент PDFium предоставляет по одному методу для каждого действия: AddPage создает пустую страницу заданного размера, AddImage (или AddPicture, если у вас уже есть TPicture) рисует растровое изображение на текущей странице, а PageNumber сообщает компоненту, на какую страницу нацелены последующие вызовы отрисовки

Единственная деталь, на которой люди спотыкаются, — это система координат. Пользовательское пространство PDF помещает начало координат в левый нижний угол страницы, при этом ось Y направлена вверх, что противоположно экранным координатам, к которым разработчики Delphi привыкли рефлекторно. Координаты X, Y, которые вы передаете в AddImage, — это нижний левый угол прямоугольника изображения, а Width, Height — это размер размещения в пунктах, а не размер исходного файла в пикселях. Если вы перепутаете это, ваши отсканированные изображения окажутся за пределами страницы или будут перевернуты по отношению к тому месту, где вы ожидали их увидеть

Создание документа и по странице на каждый скан

Начните с пустого документа. CreateDocument выделяет свежий PDF-файл и оставляет компонент активным, поэтому отдельного шага открытия нет. Отсюда вы просматриваете список отсканированных файлов и для каждого из них добавляете страницу, делаете ее текущей и размещаете изображение. Размеры страницы здесь — A4 в пунктах (595 × 842 книжная ориентация), стандартный размер листа для архивной корреспонденции

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4 width in points
  PageH = 842.0;   // A4 height in points
  Margin = 36.0;   // half-inch border around each scan
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // new, empty, already active
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-based page index
      Pdf.PageNumber := I + 1;                // make the new page current
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Каждая итерация создает страницу и сразу же устанавливает для нее PageNumber. Эта вторая строка имеет значение: AddPage вставляет страницу, но методы рисования действуют на ту страницу, которая является текущей, поэтому установка PageNumber — это то, что направляет AddImage на страницу, которую вы только что создали. Пропустите этот шаг, и ваши изображения наложатся на любую страницу, которая была загружена ранее

В этом цикле скрывается одно предположение: порядок Files. Сканер называет страницы от 0001.jpg до 0100.jpg, но перечисление каталога не всегда возвращает их отсортированными, и в тот момент, когда вы столкнетесь с page9.jpg рядом с page10.jpg, простая строковая сортировка поместит страницу 10 перед страницей 9. Явно отсортируйте список перед циклом и предпочитайте имена с нулями в начале во время сканирования, чтобы лексический порядок совпадал с порядком страниц. Последовательность страниц — это то, что рецензент замечает сразу, и это самая дешевая ошибка, которую можно предотвратить

Размещение отсканированного изображения и сохранение его пропорций

Отсканированное изображение редко имеет ту же форму, что и страница. Если вы растянете его на весь лист, вы исказите текст; если вы разместите его в полном размере в пикселях, оно выйдет за края. Решение состоит в том, чтобы масштабировать по меньшему из двух соотношений: по ширине или по высоте, и центрировать то, что осталось. Поскольку начало координат находится в левом нижнем углу, центрирование означает разделение оставшегося пространства поровну и добавление его как к X, так и к Y

procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG, etc. via the VCL graphics units

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Fit inside the margins without distorting the scan.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Center: leftover space split evenly. Y measured from the page bottom.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Это загружает файл один раз, чтобы прочитать его размеры в пикселях, вычисляет единый однородный масштаб и передает прямоугольник размещения в AddImage. AddImage напрямую принимает путь к файлу и направляет его через тот же конвейер изображений, что и AddPicture, поэтому любой формат, который распознают графические модули VCL, работает без особых случаев. Если изображение уже декодировано в TPicture из панели предварительного просмотра, вызовите AddPicture(Pic, X, Y, DrawW, DrawH) с тем же прямоугольником и пропустите второе чтение файла

Пропуск декодирования для отсканированных JPEG

Сканеры почти всегда выдают JPEG. Загрузка JPEG в TPicture декодирует его в растровое изображение, а затем PDFium повторно кодирует его при сохранении — два цикла преобразования с потерями, которые вам не нужны. AddJpegImage внедряет исходные сжатые байты прямо на страницу из потока, что и быстрее, и визуально чище для объемного пакета

var
  Stream: TFileStream;
begin
  // ... after AddPage + PageNumber for the current page ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Embeds the JPEG bytes as-is; no decode/re-encode cycle.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Вы по-прежнему вычисляете X, Y, DrawW и DrawH тем же способом, поскольку вам нужны размеры в пикселях для масштабирования. Прочитайте их из файла или с помощью быстрого разбора заголовка, затем передайте сырой поток в AddJpegImage. Для сканирований PNG или TIFF правильным путем будет AddImage; приберегите ярлык JPEG для формата, к которому он фактически применим

Маркировка каждой страницы

Архивные сканирования легче проверять, когда на каждой странице указано имя исходного файла. AddText рисует строку в координатах пространства пользователя, поэтому подпись располагается прямо под изображением. Помните о перевернутой оси Y: чтобы поместить метку под сканированным изображением, нужно вычитать из нижнего края изображения, а не прибавлять к нему

// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

И последнее замечание о сохранении. SaveAs — это функция, возвращающая логическое значение, поэтому в производственном коде проверяйте ее результат, а не предполагайте, что запись прошла успешно; в противном случае при заполненном диске или заблокированном пути вывода произойдет тихий сбой. Как только цикл завершится и файл будет записан, у вас будет именно то, что нужно архиву: один упорядоченный PDF-файл на каждое дело, страницы отмасштабированы по размеру и готовы к чтению в любом средстве просмотра

Те же строительные блоки подходят для смежных задач. Поменяйте правило определения размера для каждой страницы, и вы получите фотокнигу с одним изображением на лист; оставьте цикл, но читайте из многостраничного источника TIFF, и у вас будет конвертер факс-архивов. Если вы хотите получить более широкое представление о программном создании PDF-файлов, см. статью создание PDF-документов с нуля с помощью компонента PDFium; чтобы впоследствии отрендерить результат обратно на экран, см. статью преобразование страниц PDF в изображения JPEG с помощью компонента PDFium

Компонент PDFium от loslab.com объединяет API-интерфейсы создания документов, рендеринга и работы с текстом, используемые в этой серии статей