Команда з обробки претензій мала тридцятирічний архів паперових справ, які пропускали через потоковий сканер. Сканер зберігав кожну сторінку як окремий JPEG у папку з назвами 0001.jpg, 0002.jpg тощо. Натомість архів потребував єдиного PDF-файлу для кожної справи з правильним порядком сторінок, щоб рецензент міг відкрити один документ замість того, щоб переглядати сотні мініатюр. Саме цей останній крок, перетворення пронумерованої купи сканів у єдиний впорядкований PDF, ми і розглянемо
PDFium Component вирішує цю задачу безпосередньо. Окрім рендерингу та вилучення тексту, компонент дозволяє створювати PDF з нуля: створити порожній документ, додати чисту сторінку довільного розміру, розмістити зображення за координатами простору користувача та зберегти результат. Увесь цей конвеєр реалізовано в компоненті TPdf, тому пакетний конвертер зводиться до циклу за іменами файлів та кількох викликів методів
Загальна схема конвертації
Для кожного скану потрібно виконати три кроки. Ви визначаєте розмір сторінки, розміщуєте зображення на ній із заданими полями та переходите до наступної сторінки. PDFium Component надає відповідний метод для кожної дії: AddPage створює порожню сторінку заданого розміру, AddImage (або AddPicture, якщо ви вже використовуєте TPicture) малює растрове зображення на поточній сторінці, а властивість PageNumber вказує компоненту цільову сторінку для наступних операцій малювання
Єдина деталь, яка часто збиває з пантелику - це система координат. Простір користувача у форматі PDF розміщує початок координат у лівому нижньому куті сторінки, причому вісь Y зростає вгору, що є протилежністю до екранних координат, до яких розробники Delphi звикли на рефлекторному рівні. Значення X, Y, які ви передаєте в AddImage, є координатами лівого нижнього кута прямокутника зображення, а Width, Height - це розмір малювання в пунктах, а не в пікселях вихідного файлу. Якщо ви переплутаєте ці поняття, ваші скани опиняться за межами сторінки або будуть перевернуті
Створення документа та сторінки для кожного скану
Почніть з порожнього документа. Метод CreateDocument виділяє пам'ять під новий PDF і залишає компонент в активному стані, тому окремого кроку відкриття не потрібно. Далі ви проходите списком відсканованих файлів і для кожного з них додаєте сторінку, робите її поточною та розміщуєте зображення. Для прикладу ми використовуємо розмір сторінки А4 в пунктах (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, звичайне рядкове сортування поставить десяту сторінку перед дев'ятою. Завжди сортуйте список явно перед циклом і налаштовуйте сканер на додавання провідних нулів у назви файлів, щоб лексичний порядок збігався з фізичним. Неправильний порядок сторінок - це перше, що помітить користувач, і найпростіша помилка, якій можна запобігти
Розміщення скану та збереження його пропорцій
Пропорції скану рідко ідеально збігаються з пропорціями сторінки. Якщо розтягнути його на весь аркуш, ви спотворите текст, а якщо вставити в оригінальному розмірі в пікселях - він вийде за межі. Правильне рішення - це масштабування за меншим із двох співвідношень (за шириною або висотою) та центрування залишку. Оскільки початок координат лежить у лівому нижньому куті, центрування означає рівномірний розподіл вільного простору та додавання його до координат 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 Component. А щоб дізнатися, як пізніше відрендерити результат назад на екран, перегляньте матеріал про перетворення сторінок PDF у зображення JPEG за допомогою PDFium Component
PDFium Component від loslab.com містить усі необхідні API для створення документів, рендерингу та роботи з текстом, які використовувалися в цій серії статей