Справжнє редагування (redaction) PDF в Delphi означає видалення вихідних байтів, а не просто зафарбовування їх зверху. Бібліотека losLab PDF Library реалізує це за допомогою двох API: RedactRegion редагує потік вмісту сторінки так, що видалений текст, вектори та зображення фізично зникають, а SanitizeDocument очищає метадані, скрипти та дії, де також може ховатися конфіденційна інформація. Якщо припуститися помилки хоча б в одній із цих частин, ви надішлете файл, який виглядає цензурованим на екрані, але розкриє таємницю будь-кому, хто скопіює текст або відкриє властивості документа.
Приклади таких помилок є загальновідомими. Газети, суди та державні установи неодноразово публікували PDF-файли з чорним прямокутником поверх імені чи номера, але читачі могли легко виділити текст під ним, скопіювати його та вставити нібито видалений рядок у звичайний блокнот. Цей прямокутник був просто малюнком. Текст залишався на місці, перебуваючи в потоці вмісту на один шар нижче за фарбу, повністю неушкодженим та доступним для виділення. Бібліотека losLab PDF Library — рушій PDF для Delphi та C++Builder, доступний через клас TPDFlib, — розглядає цю різницю як головну мету редагування, а не як другорядну деталь.
Чому малювання чорного прямокутника поверх тексту PDF не є справжнім редагуванням?
Бібліотека losLab PDF Library пропонує метод MaskRect для випадків, коли закриття вмісту є саме тим, що вам потрібно: водяний знак, візуальне приховання, штамп перевірки. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) додає прямокутник до потоку вмісту сторінки та малює його оператором побудови контуру re з наступним оператором заповнення f (ISO 32000-1 §8.5). Кожен байт, який уже був на сторінці — гліфи імені, яке ви закрили, векторні лінії, вбудовані зображення — залишається на своєму місці, на одну інструкцію раніше в тому самому потоці. Сумісний переглядач малює чорний прямокутник останнім, тому він візуально перекриває інший вміст. Проте маска нічого не змінює під собою.
Будь-хто може відновити оригінальний вміст трьома способами: виділити та скопіювати текст безпосередньо через непрозору маску, запустити інструмент вилучення тексту або видалити маскуючий прямокутник із потоку вмісту. Назва методу MaskRect чесна: він маскує, а не видаляє. Результат схожий на наклейку поверх слова, яке все одно можна прочитати, якщо цю наклейку зняти.
Як саме RedactRegion видаляє вміст на рівні інструкцій
Метод RedactRegion(Page, Left, Top, Width, Height, Options) працює на один шар глибше за малювання. Бібліотека losLab PDF Library розбирає кожен шар вмісту сторінки в об'єкт TPDFContentProgram, обходить список інструкцій за допомогою аналізатора CTM (FindInstructionsInRect у моделі вмісту) і помічає кожен оператор відображення тексту, малювання контуру та відображення зображення Do, область малювання якого перетинається з вашим прямокутником. Ці інструкції видаляються за допомогою TPDFContentProgram.Delete, а відредагований шар записується назад за допомогою WritePageContentLayer. Байти не ховаються за обкладинкою — вони повністю видаляються з потоку. Запущений після цього інструмент вилучення тексту нічого не знайде, бо шукати більше нічого.
Варто звернути увагу на дві деталі реалізації. Метод RedactRegion приймає прямокутник з початком координат у лівому верхньому кутку в поточних одиницях вимірювання та внутрішньо перетворює його на простір користувача з початком у лівому нижньому кутку, який використовується потоком вмісту, тому ви можете вказати область саме так, як бачите її на сторінці. Коло прямокутник охоплює кілька інструкцій, RedactRegion видаляє їх у порядку спадання індексів, оскільки видалення інструкції 4 перед інструкцією 7 змістило б усі наступні індекси та порушило б діапазон. При передачі Options = 0 бібліотека losLab PDF Library також малює суцільний чорний прямокутник поверх очищеної області як видимий маркер, але цей маркер є суто косметичним і накладається вже після того, як справжній вміст повністю видалено.
На практиці ви рідко кодуєте координати прямокутника вручну. Ви шукаєте на сторінці конфіденційний рядок і зчитуєте його обмежувальну рамку (bounding box) — той самий пошук тексту та перерахування елементів сторінки, які описані в статті про пошук тексту та елементів сторінки в Delphi, — а потім передаєте цей прямокутник методу RedactRegion:
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('contract.pdf', '') = 1 then
begin
// Delete everything painted inside a 220 x 14 pt box on page 1.
// Coordinates are top-left, in the current origin and units.
PDF.RedactRegion(1, 90, 705, 220, 14, 0); // Options=0 stamps a black marker
PDF.SaveToFile('contract-redacted.pdf');
end;
finally
PDF.Free;
end;
end;
Де ще в PDF можуть ховатися конфіденційні дані?
Редагування, яке обмежується лише видимим шаром, залишає сліди, оскільки PDF зберігає інформацію в місцях, куди звичайний читач ніколи не заглядає. Бібліотека losLab PDF Library виділяє кілька каналів таких витоків. Словник інформації про документ (Document Information Dictionary) та потік метаданих XMP зазвичай містять поля «Автор» (Author), «Виробник» (Producer) або «Ключові слова» (Keywords), які можуть містити імена або назви справ, які ви вилучили з тексту. Скрипти JavaScript на рівні документа та каталог OpenAction можуть виконуватися під час відкриття файлу та отримувати доступ до даних, які ви вважали видаленими. Кожна анотація містить необов'язкову дію /A, а сама анотація (нотатка, застаріле поле форми, посилання) може знаходитися безпосередньо поверх відредагованого місця та містити власну копію тексту. Ніщо з цього не захищається шифруванням: зашифрований файл із відкритим пакетом XMP все одно передає свій заголовок будь-якому індексатору, що є приводом для аудиту шифрування та прав доступу. Надійне очищення має перевіряти кожне з цих прихованих місць, а не лише сторінку, яку ви бачите на екрані.
Очищення документа за допомогою SanitizeDocument
Бібліотека losLab PDF Library вирішує проблему витоків за допомогою методу SanitizeDocument(Flags) — виклику, який запускає набір незалежних операцій очищення, вибраних за допомогою бітової маски, і повертає кількість застосованих операцій. Прапорці можна комбінувати. SANITIZE_JAVASCRIPT видаляє кожен пакет JavaScript на рівні документа та будь-які скрипти OpenAction. SANITIZE_ACTIONS видаляє словник дій /A з кожної анотації за допомогою нового методу TPDFAnnots.RemoveAnnotAction та очищає каталог OpenAction. SANITIZE_METADATA очищає поля Author, Subject, Keywords, Creator та Producer як у словнику інформації про документ (ISO 32000-1 §14.3.3), так і в потоці метаданих XMP (§14.3.2). SANITIZE_ANNOTATIONS видаляє всі анотації в усьому документі, а SANITIZE_OPEN_ACTION вилучає запис /OpenAction із каталогу. SANITIZE_ALL є побітовим об'єднанням усіх п'яти прапорців.
Правила індексації всередині цих операцій є саме тими нюансами, які зазвичай ламають власноруч написані цикли, і саме тому SanitizeDocument реалізовано як один виклик. Індексація у GlobalJavaScriptPackageName починається з 0, а метод RemoveGlobalJavaScript зменшує список у процесі роботи, тому операція очищення JavaScript завжди зчитує пакет з індексом 0 і видаляє його, поки лічильник не досягне нуля. Натомість метод DeleteAnnotation використовує індексацію з 1. Властивість AnnotationCount повертає значення для поточної сторінки, тому операція очищення анотацій вибирає кожну сторінку по черзі перед підрахунком. Бібліотека losLab PDF Library правильно обробляє ці нюанси внутрішньо, тому ви просто передаєте набір прапорців і отримуєте результат, не реалізуючи п'ять різних правил обходу елементів.
var
Applied: Integer;
begin
// Strip metadata, scripts, and actions but keep the annotations:
Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);
// Or clear every hidden channel, annotations included:
Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;
Повний процес редагування та очищення
Якщо об'єднати обидва кроки, повна процедура виходить досить короткою. Бібліотека losLab PDF Library видаляє видимий вміст сторінка за сторінкою за допомогою RedactRegion, а потім очищає всі приховані канали за допомогою SanitizeDocument(SANITIZE_ALL) перед записом файлу. Оскільки редагування майже завжди виконується для документів, отриманих із неконтрольованих джерел, варто завантажувати їх через захищений шлях — так само, як описано в статті про безпечний аналіз ненадійних PDF-файлів, щоб некоректний вхідний файл викликав чітку помилку завантаження, а не збій посеред процесу редагування.
var
PDF: TPDFlib;
Applied: Integer;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('incoming.pdf', '') = 1 then
begin
PDF.RedactRegion(1, 90, 705, 220, 14, 0); // delete the sensitive line
Applied := PDF.SanitizeDocument(SANITIZE_ALL); // clear metadata, JS, actions, annots
PDF.SaveToFile('published.pdf');
end;
finally
PDF.Free;
end;
end;
Межі застосування редагування потоку вмісту
Метод RedactRegion працює з потоком вмісту, тобто саме там, де знаходиться цифровий текст, і де немає відсканованого тексту. Коли сторінка є відсканованим зображенням, слова є пікселями всередині об'єкта зображення XObject, а не текстовими операторами, і бібліотека losLab PDF Library не може вирізати ім'я з середини растру так, як вона видаляє гліф. Робота із зображеннями тут є навмисно консервативною: будь-яке зображення, яке перетинається з прямокутником редагування, видаляється повністю, тому спроба редагувати область на відсканованій сторінці призведе до видалення всього скану сторінки, а не окремої її частини. Редагування області всередині зображення вимагає його растрування, зафарбовування та повторного вбудовування на піксельному рівні, що є зовсім іншою операцією, ніж видалення з потоку вмісту. Таке ж застереження стосується і шару тексту OCR поверх відсканованої сторінки: RedactRegion видаляє текстові оператори, які він бачить, але зображення під ними залишається недоторканим, якщо його об'єкт XObject не перетинається з прямокутником.
Два додаткових нюанси забезпечують надійність результату. Тест на перетин оцінює ширину кожного текстового блоку приблизно на основі кількості символів та розміру шрифту, а не шляхом точного вимірювання метрик гліфів, тому дуже коротке слово пропорційним шрифтом може вийти за межі тісного прямокутника — залишайте невеликий запас для рамки. Видалення також відбувається на рівні окремих інструкцій: RedactRegion видаляє оператори відображення тексту повністю, тому прямокутник, який зачіпає лише частину текстового блоку, може видалити і сусідні символи, які оброблялися тим самим оператором. Жодне з цих обмежень не є причиною відмовлятися від механізму, це лише привід ретельно підбирати межі прямокутника та перевіряти результат.
Обов'язковим кроком будь-якого процесу редагування є перевірка. Застосуйте редагування області, виконайте очищення документа, збережіть у новий файл, відкрийте його знову та спробуйте вилучити текст, який ви видалили, та метадані, які ви очистили. Якщо вони порожні, то редагування пройшло успішно. Інтерфейси API RedactRegion та SanitizeDocument постачаються разом із бібліотекою losLab PDF Library для Delphi та C++Builder разом із повним довідником по моделях вмісту та очищенню.