Генерація звіту зводиться до розміщення на сторінці трьох речей і узгодження того, де саме вони сидять: текст у відомих координатах, шрифти, що рендеряться однаково і на сервері, і на вашому робочому столі, та зображення, підігнані під розмір. Усе інше, що робить бібліотека звітів, вибудовується навколо цих трьох. HotPDF, бібліотека генерації PDF від losLab для Delphi та C++Builder, дає кожну з них як прямий виклик на об'єкті сторінки, і єдине справжнє тертя — це система координат під усім цим, що працює в протилежний бік порівняно зі звичним вам полотном VCL. Розберіться з цією орієнтацією спочатку — і решта роботи з розміткою перестане з вами боротися
Розміщення тексту та початок координат у нижньому лівому куті
Майже в усіх перший звіт виходить перевернутим догори дриґом. Заголовок опиняється біля нижнього краю, а кожен наступний рядок піднімається до верху. Нічого не зламано. Простір користувача PDF, визначений в ISO 32000-1 §8.3, розташовує початок координат у нижньому лівому куті з Y, що зростає вгору, — це дзеркальне відображення полотна GDI, де Y зростає вниз від верхнього лівого кута. П'ять хвилин, витрачених на примирення з цим, рятують від переписування розмітки в той момент, коли числа перестають мати сенс
Центральний виклик об'єкта сторінки — TextOut(X, Y, Angle, Text). X і Y розташовують текст у пунктах від нижнього лівого кута, а Angle обертає його в градусах, і саме так малюється діагональний штамп DRAFT чи COPY без жодної спеціальної підтримки. Хитрість, що дозволяє інтуїції, натренованій на VCL, і далі працювати, — виражати Y як висоту сторінки мінус бажану відстань від верху:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-0001.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE'); // 50pt від верху Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // повернутий штамп
Pdf.AddPage; // CurrentPage тепер вказує сюди
Pdf.CurrentPage.SetFont('Arial', [], 10); // стан шрифту не переноситься
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Дві стани-залежні поведінки в цьому лістингу відповідальні за більшість помилок, що проявляються лише на другій сторінці. AddPage перенаправляє CurrentPage на щойно створену сторінку, тож посилання на сторінку, яке ви закешували раніше, більше не малює там, де ви очікуєте. Вибір шрифту теж прив'язаний до сторінки, а не до документа. Якщо пропустити SetFont після AddPage, перший TextOut на новій сторінці відкотиться до того значення за замовчуванням, з яким сторінка почалася, а не до жирного шрифту заголовка, який ви встановили три сторінки тому. Безпечна звичка — трактувати «почати нову сторінку» і «відновити стан тексту» як один нерозривний крок у циклі звіту
Шрифти, що існують на сервері, а не лише на вашому робочому столі
Більшість проблем зі шрифтами — це насправді проблеми розгортання в маскуванні. На вашій розробницькій машині встановлено корпоративний шрифт, тож звіт виглядає правильно на вашому екрані й іде в реліз. Продакшн-хост запускає завдання під сервісним обліковим записом, який ніколи не мав цього шрифту встановленим, рендерер тихо підміняє щось, що зможе знайти, і першим, хто про це почує, стане клієнт, який запитає, чому змінився фірмовий бланк. Вихід — перестати довіряти каталогу шрифтів ОС і завантажувати шрифт із файлу, який ваш інсталятор кладе на диск. Виклик реєстрації Unicode у HotPDF приймає шлях і робить саме це:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));
TextOut приймає WideString напряму, і це важить більше, ніж здається на перший погляд. Ім'я клієнта з діакритикою, німецька вулиця, польське місто: це не крайні випадки, це звичайний вміст таблиці клієнтів, і вони проходять через той самий виклик, що й жорстко закодовані ASCII-мітки, за умови що зареєстрований шрифт справді містить потрібні гліфи. З вбудованими шрифтами йде одне обмеження версії: документ має бути PDF 1.5 чи новішим, тож якщо якась незв'язана вимога прив'язує вас до старішої версії, саме це тихо зламається. Скрипти справа наліво, як-от арабська та іврит, потребують справжнього шейпінгу, а не прямого пошуку гліфів, і для цього є окремий конвеєр; див. нашу статтю про шейпінг тексту складних скриптів у HotPDF
Коли жоден встановлений шрифт не може виразити те, що вам потрібно, — думайте про символи MICR на чеку чи власний набір символів, — прогалину заповнюють шрифти Type 3. Ви визначаєте кожен гліф як маленький потік вмісту через RegisterType3Font і AddType3Glyph. Це спеціалізований куточок API, до якого ви звертатиметеся рідко, але він набагато чистіший, ніж розсипати сотні дрібних растрових символів по сторінці
Зображення: середні аргументи — це ширина й висота, а не кут
Робота із зображеннями розділяється на два кроки, і тримати їх окремо — це і є весь сенс. AddImage приймає TBitmap чи TJPEGImage, вбудовує його один раз і повертає індекс. Графіку PNG перед цим треба декодувати в растр. Далі ShowImage малює цей індекс де завгодно й скільки завгодно разів. Порядок аргументів у ShowImage — саме те місце, де варто пригальмувати й уважно прочитати:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // декодувати PNG у растр
LogoIdx := Pdf.AddImage(Logo, icFlate); // без втрат для плоскої графіки
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle): не (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
Два числа після позиції — це ширина й висота. Це не координати протилежного кута, а завершальний аргумент — кут повороту в градусах. Прочитайте сигнатуру як бокс X1/Y1/X2/Y2, і логотип 120 на 40, розміщений у (50, 700), натомість розтягнеться звідти до (120, 40), розповзаючись по більшій частині сторінки. Результат робить помилку очевидною, тоді як вихідний код виглядає цілком розумно, — саме це й змушує змарнувати цілий день. KeepImageAspectRatio за замовчуванням дорівнює True, тож бокс із неправильними пропорціями обрамляє зображення полями замість того, щоб його спотворювати; перемкніть на False лише тоді, коли ви справді маєте намір розтягнути
Поділ на реєстрацію та розміщення окупається на довгих прогонах. Оскільки AddImage вбудовує пікселі один раз, а кожен ShowImage із цим індексом вказує назад на той самий вбудований об'єкт, місце виклику AddImage визначає розмір файлу. Викличте його всередині циклу сторінок для 500-сторінкової виписки — і той самий логотип вбудується 500 разів. Викличте його один раз перед циклом, збережіть індекс — і логотип буде збережено лише один раз. Невеликий словник, ключем якого є шлях до ресурсу, достатній, щоб гарантувати, що кожне окреме зображення зареєстровано рівно один раз
Вибір кодека — ще один важіль розміру. Фотографічний вміст, скановані вкладення тощо, — місце для JPEG: передайте icJpeg у AddImage і опустіть JpegQuality приблизно до 85, оскільки властивість починається зі 100, а різниця при 85 на друкованій сторінці непомітна. Плоска графіка на кшталт логотипів, діаграм і лінійних малюнків — місце для icFlate, де стискання без втрат уже компактне, а JPEG розмазав би видиме кільцювання навколо різких країв. Прогін виписок, що штовхає по одному фото повної якості на кожну сторінку, може роздутися до гігабайтів; той самий вміст у JPEG 85 приземляється приблизно на десяту частку розміру, і жоден читач цього не помітить
Лінії, прямокутники й затінення за допомогою примітивів шляху
Горизонтальна лінія під заголовком таблиці та сірий прямокутник за підсумковою цифрою не мусять бути зображеннями. Намалюйте їх як вектори — і вони лишаються чіткими за будь-якого масштабування, різко друкуються й майже нічого не додають до розміру файлу. HotPDF дотримується тієї самої моделі, що й сирі потоки вмісту PDF: побудувати шлях, а потім викликати оператор, що його малює
// Горизонтальна лінія під заголовком таблиці
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Затінений бокс підсумків: X, Y, ширина, висота
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
Порядок тут не факультативний: спершу встановити стан фарби, побудувати шлях, а потім викликати Stroke чи Fill. Шлях, який ви побудували, але жодного разу не пофарбували, нічого не додає до сторінки, і це майже завжди відповідь, коли лінія "не з'являється". SetRGBFillColor приймає одне значення TColor, тож звичні константи VCL на кшталт clNavy і clBlack вставляються напряму, а Rectangle використовує ті самі аргументи ширини й висоти, що й розміщення зображень, а не два кути. Одна засторога щодо тонких ліній: усе, що тонше приблизно за пів пункту, може виглядати елегантно на моніторі, а потім зникнути на офісному принтері 600 dpi, тож 0,75pt — розумна нижня межа для будь-якої лінії, що має пережити друк
Пагінація на реальних даних, а не на демонстраційних
Одна деталь, яку варто зробити правильно до того, як розмітка застигне: числові колонки мають вирівнюватися по правому краю, і спосіб зробити це — виміряти відрендерену ширину кожного значення й позиціонувати його назад від межі колонки, а не доповнювати рядок пробілами спереду. Доповнення пробілами вирівнюється лише в моноширинному шрифті, а фінансовий звіт ніхто не набирає моноширинним шрифтом. Спершу пропустіть значення через локалезалежні процедури Delphi на кшталт FormatFloat, щоб роздільник тисяч, ширину якого ви вимірюєте, був точно тим самим, що його справді покаже локаль клієнта
Небезпека з пагінацією в тому, що ви пишете її проти демонстраційного набору даних, де десять коротких рядків уміщуються на одній сторінці й циклу ніколи не доводиться розриватися. Продакшн дає вам клієнта, чия назва компанії тягнеться на 140 символів, і виписку з 4000 рядків, і тепер цикл мусить розриватися правильно щоразу. Патерн, що витримує перевірку, — це один курсор Y, що рухається вниз, коли ви віднімаєте висоту кожного рядка, і перевірка, що починає нову сторінку в момент, коли курсор мав би перетнути нижнє поле. «Вниз» тут означає зменшення Y, і це те єдине місце, де початок координат у нижньому лівому куті лишається контрінтуїтивним. Тримайте все це в одній процедурі, яка також повторно викликає SetFont і перемальовує наскрізний заголовок на новій сторінці, і помилки зсуву на одну сторінку ніколи не знайдуть опори. Коли ті самі звіти мають також задовольняти архівні правила чи правила доступності, саме вибори, які ви робите тут: які шрифти вбудовуєте, чи тегований вивід, які колірні простори використовуєте, — це якраз те, що ці стандарти контролюють; посібник HotPDF з PDF/A, PDF/X і PDF/UA варто прочитати до того, як шаблон затвердне
Кожен показаний тут виклик — позиціонування тексту, реєстрація шрифтів, вбудовування зображень і малювання шляхів — постачається в HotPDF Delphi Component для Delphi та C++Builder, чий довідник документує повний API виводу поряд із функціями форм, шифрування та підписання, які стоять поруч