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

Редагування PDF і складання N-up у Delphi з HotPDF

Надходить запит: взяти пакет уже відрендерених виписок, затемнити номери рахунків і розмістити по дві сторінки на аркуші, щоб зекономити папір. Обидві частини цього завдання - це хірургія потоку вмісту в PDF, який ви не створювали, тож тут немає зручного полотна сторінки для малювання і немає менеджера шрифтів, на який можна спертися. Ви безпосередньо редагуєте граф об’єктів завантаженого документа, додаючи сирі оператори малювання на сторінку, яку верстала інша програма. HotPDF відкриває саме два входи для цього, і небезпечнішим із них є той, що виглядає безневинним

HotPDF - це нативний VCL PDF-компонент для Delphi і C++Builder. Його API завантажених документів у дев’ятому випуску додав перші методи, які створюють новий вміст на сторінці, яку ви відкрили з диска, а не зібрали з нуля. Два з них є темою цієї статті: RedactLoadedRect, який наносить непрозорий прямокутник поверх області, і StitchLoadedPage, який масштабує одну сторінку та виводить її на іншу. Обидва працюють, записуючи оператори потоку вмісту ISO 32000-1 §8.5 у /Contents потік сторінки. Розуміння того, що ці оператори роблять, і не менш важливо, чого вони не роблять, є різницею між робочим інструментом і витоком даних

Додавання операторів до завантаженої сторінки

Коли ви створюєте сторінку через звичайний API HotPDF, компонент керує потоком вмісту і серіалізує за вас ваші TextOut текстові та векторні виклики. Завантажена сторінка інша: її /Contents - це наявний об’єкт потоку, можливо спільний, можливо частина масиву вмісту, і вам потрібно вбудуватися в нього, не пошкодивши те, що вже там є. У дев’ятому випуску з’явилися три невеликі допоміжні функції, які роблять це безпечно. NewIndirectStream виділяє новий непрямий THPDFStreamObject з порожнім буфером і записом /Length 0; ResolveLoadedStream спускається за непрямим посиланням до базового потоку; а AppendLoadedStream записує сирі байти в кінець потоку і переписує /Length так, щоб збережений об’єкт залишався коректно сформованим

Патерн, якого дотримуються обидва публічні методи, однаковий. Знайдіть /Contents сторінки, розв’яжіть його до потоку, і якщо придатного потоку немає, створіть його та приєднайте. Потім додайте оператори. Оскільки нові байти потрапляють у кінець потоку, модель малювання гарантує, що вони відображатимуться поверх усього, що намалювала початкова верстка. Саме цей порядок і є механізмом прямокутника маскування, і саме тому цей прямокутник не є тим, чим його зазвичай вважають

RedactLoadedRect: непрозоре покриття, а не видалення

RedactLoadedRect приймає нульовий індекс сторінки, чотири координати в просторі користувача і три компоненти кольору в діапазоні 0–1:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Під капотом метод записує три оператори у потік вмісту: встановлення кольору заливки в DeviceRGB (r g b rg), шлях прямокутника (x y w h re), і заливку (f). Ширина та висота обчислюються як X2 - X1 і Y2 - Y1, тож ви передаєте дві протилежні вершини і дозволяєте методу обчислити розмір. Якщо передати 0, 0, 0 для кольору, отримаєте чорну смугу; якщо передати 1, 1, 1 для білого, отримаєте білу смугу, що збігається з білою сторінкою. Координати належать власному простору користувача завантаженої сторінки, а це означає, що початок координат знаходиться в лівому нижньому куті, а одиниці вимірювання - пункти, і також означає, що для точного розміщення будь-чого вам потрібен /MediaBox сторінки; GetLoadedPageBox з pbMediaBox дає вам саме це

Прочитайте це двічі: заповнений прямокутник візуально перекриває вміст, але не видаляє його. Текст, зображення або векторна графіка під прямокутником і далі присутні в PDF, і далі є в графі об’єктів, і далі можуть бути витягнуті будь-ким, хто скопіює сторінку, запустить текстовий екстрактор або просто видалить ваш прямокутник із потоку вмісту. Це візуальне маскування, а не редагування у юридичному чи безпековому сенсі. Якщо ви ховаєте справді чутливі дані - номери рахунків, медичні записи, ідентичності, будь-що регульоване - накрити їх чорним блоком і надіслати файл означає залишити витік даних, який рано чи пізно знайдуть. Справжнє редагування вимагає видалення базових об’єктів вмісту, а не зафарбовування поверх них

Назва методу каже "Redact", і це корисне попередження про те, як результат буде неправильно прочитаний, а не обіцянка щодо того, що він видаляє. Реалізація чесно говорить про це у власному коментарі: вона називає себе "visual redaction primitive" і зазначає, що redaction з видаленням вмісту потребує інтерпретатора content-stream, який проходить наявні оператори та переписує їх. Шлях HotPDF для завантаженого документа цього тут не робить. Тож безпечне правило вузьке: використовуйте RedactLoadedRect для не чутливого косметичного маскування - приховати draft watermark, очистити область перед screenshot, накрити застарілий logo на internal proof. Щойно те, що лежить під прямокутником, стане важливим у разі витоку, цей метод - неправильний інструмент, а правильна відповідь - згенерувати документ заново без цих даних або скористатися справжнім pipeline для видалення вмісту

StitchLoadedPage: масштабувати, змістити, намалювати

N-up imposition - простіша задача, бо тут нічого не ховається, а лише переставляється. StitchLoadedPage приймає індекс цільової сторінки, індекс вихідної сторінки, зміщення X/Y та коефіцієнт масштабу і малює вихідну сторінку в цій позиції та з цим розміром:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

Рядок операторів, який він дописує, - це стандартна послідовність transform-and-paint: q щоб зберегти graphics state, cm matrix, що несе масштаб по діагоналі та зміщення в слотах трансляції, /StitchSrc Do щоб викликати external object, і Q щоб відновити state. Пара q/Q пара має значення: вона ізолює transform, щоб stitched page не занесла свою систему координат у все, що буде дописано після неї. Метод також захищає від очевидних помилок - індекси поза діапазоном, ціль, що дорівнює джерелу, непозитивний scale (який він обмежує до 1.0) - і виходить мовчки замість того, щоб генерувати помилку, тож перевіряйте вхідні дані, бо silent no-op виглядає так само, як успіх

StitchLoadedPageSideBySide - це тонка зручна обгортка над загальним методом. Вона читає ширину media-box цільової сторінки, ділить її навпіл і викликає StitchLoadedPage з цією половинною шириною як X-зміщенням і фіксованим scale 0.5, розміщуючи джерело в правій половині. Це жорстко задане 0.5 припускає, що source і target мають однакову ширину; якщо ні, джерело не заповнить свою половину акуратно, і вам знадобиться загальний StitchLoadedPage з scale, який ви обчислите самі з обох media-box

Спрощена стратегія XObject та її ISO-компроміс

Ось тут реалізація свідомо робить спрощення, про яке треба знати, перш ніж довіряти результату в різних переглядачах. Правильний N-up imposition загортає вміст сторінки-джерела у Form XObject - самодостатній drawable object, який ISO 32000-1 §8.10.1 вимагає доповнити /Type /XObject, /Subtype /Form, та власним /BBoxобмежувальним прямокутником. HotPDF's round-nine stitch не будує цю обгортку. Натомість він реєструє вихідний словник сторінки як такий безпосередньо під цільовим /Resources /XObject як назву StitchSrc, а потім малює його за допомогою Do. Словник сторінки і Form XObject досить сильно поділяють свою модель вмісту - обидва посилаються на потік вмісту і словник ресурсів - тому багато переглядачів відрендерять результат

Але це не Form XObject, що відповідає вимогам. Йому бракує /Subtype /Form маркера та власного /BBox, а це означає, що суворий споживач має повне право проігнорувати Do або обрізати його інакше, ніж ви очікуєте. У TechnicalNotes для цього раунду сказано це прямо: підхід "renders under most readers" є "not a strictly ISO-compliant Form XObject", а повна відповідність вимагає окремим кроком синтезувати справжній потік Form XObject. Тож ставтеся до stitch-виводу так само, як до будь-якої невідповідної конструкції: перевіряйте його у тих самих переглядачах, які запускають ваші клієнти, а не лише в тому, що стоїть на вашій машині, і якщо вам потрібні архівні PDF або PDF, чисті для строгих валідаторів, не покладайтеся на цей шлях. Та сама дисципліна стосується всього, що ви будуєте на завантаженому графі об'єктів, тому PDF preflight pass in Delphi заслуговує на місце у release pipeline щоразу, коли ви programmatically змінюєте документи

Де це доречно, а де ні

Обидва методи є інструментами для content-stream, тож mental model та сама, яку ви використовуєте для direct drawing. Якщо ви будували сторінки з нуля за допомогою компонента, vector і colour-оператори за цими викликами будуть знайомі з HotPDF canvas drawing in Delphi; різниця лише в тому, що тут ви дописуєте у потік, який хтось інший авторував, а не у власний. Тримайте в голові три межі:

  • Редагування - це косметика. RedactLoadedRect малює поверх вмісту і ніколи його не видаляє. Для будь-яких чутливих даних згенеруйте джерело заново або використайте справжнє видалення вмісту - чорний прямокутник не є захистом
  • Stitch - це невідповідний за задумом. Вихідну сторінку посилають як псевдо-XObject без §8.10.1 /Subtype /Form та /BBox, тож перевіряйте відображення у ваших цільових переглядачах і уникайте цього, коли потрібна сувора перевірка
  • Координати задаються в просторі користувача сторінки. Початок координат у нижньому лівому куті, одиниці виміру: пункти, що визначаються власним media box сторінки. Спочатку прочитайте box за допомогою GetLoadedPageBox перед тим, як щось розміщувати, бо завантажена сторінка може бути не того розміру, який ви припускали

Використані в цих межах, ці дві можливості покривають реальний робочий процес: переставляти сторінки для друку, приховувати неконфіденційні ділянки та записувати результат назад за допомогою SaveLoadedDocument - без повного повторного рендерингу. API для завантажених документів, що включає ці примітиви stitch і mask, постачається разом із HotPDF Component для Delphi і C++Builder, а також методи для полів форм, анотацій і FDF з того самого випуску