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

Автоматизація перевірок PDF Preflight у Delphi з HotPDF

Файл бездоганно відкривається на вашій машині. Acrobat показує його, попередній перегляд друку виглядає правильно, усі сторінки на місці. Потім він потрапляє в друкарське бюро або в систему архівування, що приймає вашу місячну партію, — і повертається відхиленим: RGB-зображення в завданні CMYK, відсутній ключ /Trapped, output intent, що не відповідає пресі. Ніхто не міг побачити в документі жодної вади. Він виявився неправильним проти профілю, а профіль перевірили там, де вас не було. Preflight — це поліграфічна назва такої перевірки, і справжнє питання — де їй місце, коли PDF виходять із вашого власного коду Delphi, а не з робочого столу дизайнера

HotPDF не дає функції preflight для виклику. Компонент несе вікно звіту preflight у своїй GUI-демці, але за ним не стоїть жодного API, який міг би викликати сервіс чи скрипт збірки, і вдавати інше означало б відправити вас шукати метод, якого не існує. Це звучить як прогалина, доки не помітити, що для файлів, які ви генеруєте самі, виклик валідатора на власному виводі — узагалі неправильна форма. Ви вже контролюєте кожну властивість, яку перевірив би валідатор. Корисний поділ — зробити генератор нездатним видати поганий файл, а потім довести це інструментом, який ви не писали

Діаграма конвеєра preflight у Delphi: налаштування відповідності HotPDF запобігають поганим PDF під час генерації, а veraPDF і Acrobat Preflight зовнішньо доводять якість результату
Запобігання вписує правила PDF/A і PDF/X у саму генерацію HotPDF, тоді як зовнішні валідатори постачають вердикт, який генератор не здатен виставити собі сам

Чому власний вивід перевіряють інакше

Традиційний preflight передбачає файл чужинця. Його створив якийсь дизайнер, якийсь інший застосунок, якийсь невідомий ланцюжок правок, і ви перевіряєте його, бо не маєте жодного уявлення, що всередині. Документ, створений вашим кодом, — не чужинець. Вбудовування шрифтів, колірний простір, output intent, блок метаданих: ваша програма вирішила все це за кілька мілісекунд до того, як файл торкнувся диска. Перевіряти його потім, щоб виявити рішення, які ви щойно прийняли, — марна робота. Дешевший хід — обмежити ці рішення так, щоб невідповідного файлу взагалі не існувало для перехоплення

Є й причина довіри тримати перевірку зовнішньою. Бібліотека, що благословляє власний вивід, сама собі оцінює власний іспит. Коли система архівування клієнта чи RIP друкарні відхиляє ваш файл, фраза «наш компонент каже, що все гаразд» не має жодної ваги. А вердикт від veraPDF чи Acrobat має, бо інша сторона запускає ті самі інструменти

Зробіть відповідність налаштуванням, а не чеклістом

Рівень запобігання — це просто конфігурація. Встановіть PDFACompliance чи PDFXCompliance до BeginDoc, і HotPDF дотримується відповідних правил протягом усього проходу генерації: він вбудовує шрифти, стежить за використанням DeviceRGB і DeviceCMYK щодо задекларованого вами output intent і відмовляється від функцій, заборонених профілем. Суперечності виринають на EndDoc, де бар'єри відповідності піднімають виняток замість того, щоб мовчки відправити щось, що провалиться нижче за конвеєром. Щойно файл збережено, ті самі властивості зчитуються назад і показують, що насправді було примусово застосовано, — і це той єдиний факт, який найбільше потрібен вашому журналу конвеєра:

// Після EndDoc: записати застосовані профілі разом із метаданими запуску
if Pdf.PDFACompliance <> '' then
  Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
  Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);

Розмістіть ці прапорці в тому самому рядку журналу, що й хеш вхідних даних та версію HotPDF. У той день, коли валідатор і ваш генератор розійдуться в думках щодо файлу, цей рядок скаже вам, який шаблон його породив і яку збірку бібліотеки було завантажено, і суперечка, яка інакше з'їла б цілий день, перетвориться на grep. Output intent, ICC-профілі й тегування, що стоять за цими прапорцями, детально розписані в посібнику з виводу PDF/A, PDF/X і PDF/UA у HotPDF

Дешевий перший бар'єр для файлів, які ви не генерували

Не кожен конвеєр суто генеративний. Клієнти завантажують PDF, сканери скидають їх у папку, партнери прикріплюють до листів. Проганяти кожен із них через повний структурний валідатор — марнувати час черги на файли, які навіть не відкриються. Direct File API у HotPDF читає достатньо структури файлу, щоб відповісти на питання «чи це взагалі придатний PDF», не завантажуючи все дерево об'єктів, що робить його добрим місцем для швидкого провалу:

function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
  Handle, Pages: Integer;
begin
  Result := False;
  Handle := Pdf.DAOpenFileReadOnly(FileName, '');
  if Handle <= 0 then
    Exit;  // структурно нечитабельний: карантин, не перевіряти
  try
    Pages := Pdf.DAGetPageCount(Handle);
    Result := Pages > 0;
  finally
    Pdf.DACloseFile(Handle);
  end;
end;

Два факти про цей API визначають, як його обгортати. Скорочення з плоскою пам'яттю тримається лише для незашифрованих вхідних даних; передайте DAOpenFileReadOnly пароль — і він тихо відкотиться до повного розбору, тож файл, про який ви знаєте, що він зашифрований, слід спершу пропустити через DecryptFile у звичайну робочу копію перед сортуванням. І DAGetPageCount нічого не означає на дескрипторі, що не відкрився бездоганно, тож перевірка дескриптора лишається строгою, а недодатний результат — це відхилення, а не повторна спроба. Більше таких патернів — у статті про Direct File API для роботи з великими PDF

veraPDF, запущений як частина збірки

Для всього, що ви заявляєте як PDF/A чи PDF/UA, veraPDF — це валідатор, який варто підключити. Він працює без графічного інтерфейсу, приймає партію, видає XML чи JSON і називає кожен збій за його пунктом ISO, тож порушення правила проти пункту 6.2.2 ISO 19005-1 прямо вказує на налаштування генератора, а не залишає вас гадати. Керувати ним з Delphi — це звичайне керування процесами:

function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
  Cmd: string;
  SI: TStartupInfo;
  PI: TProcessInformation;
begin
  Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
    [PdfFile, ReportFile]);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
      CREATE_NO_WINDOW, nil, nil, SI, PI) then
    RaiseLastOSError;
  try
    WaitForSingleObject(PI.hProcess, 120000);  // обмежити очікування на файл
    GetExitCodeProcess(PI.hProcess, Result);
  finally
    CloseHandle(PI.hThread);
    CloseHandle(PI.hProcess);
  end;
end;

Цей таймаут відпрацьовує своє утримання. Пошкоджений файл може загнати будь-який парсер у кут, з якого той ніколи не вибереться, а необмежене очікування всередині воркера черги тягне за собою решту черги. Обмежте очікування, дайте таймауту власний код збою і відкладіть файл для людини. Коли читаєте результат, розбирайте XML на ідентифікатори правил, а не на текст для людини. ID правил переживають оновлення валідатора; формулювання повідомлень — ні, а стабільний код — це те, за чим інженер підтримки може шукати в старих тікетах

Те, як ви запускаєте партію, важить не менше, ніж те, чи проходить кожен файл. Один процес на файл, а не один на партію, щоб отруйний вхід коштував вам лише таймаут цього файлу й нічого більше. Обмежте кількість процесів валідатора кількістю ядер, бо побудова звіту XML прив'язана до процесора, а перепідписка лише спричиняє метушню. І поставте стелю розміру на вході, бо двогігабайтна скановна книга захопить чергу незалежно від того, наскільки терплячий парсер. Ніщо з цього не є preflight у строгому сенсі. Це різниця між бар'єром, що переживає обсяг наприкінці місяця, і тим, який вимикають першої ж ночі, коли він застопорює конвеєр о другій ночі

Діаграма пакетного шлюзу Delphi: один процес veraPDF на PDF з обмеженим тайм-аутом, вилучення ID правил із XML замість повідомлень і архівування кожного звіту поруч із його файлом
Вхідна охорона обмежує навантаження черги, а свіжий працівник veraPDF на файл не дає отруєному входу затримати збірку

Саме тут це дає збій — на PDF/X. veraPDF його не перевіряє, тож робочою перевіркою й досі лишається Preflight в Acrobat із профілем ISO 15930, який назвав ваш друкар. Acrobat потребує людину, а це означає вибіркове охоплення замість повного: перший файл із нового шаблону плюс невелика випадкова вибірка з кожної партії, тоді як автоматизований бар'єр опрацьовує все, що можна опрацювати без людини. Вибіркова перевірка, яка справді працює, краща за повну автоматизацію, що назавжди лишається недороблено на половині

Звіт, який вам знадобиться і через рік

Бар'єр preflight окупається двічі. Один раз, коли він зупиняє поганий файл на вході, і вдруге — набагато пізніше, коли хтось запитає, чому конкретний файл пропустили. Саме цей другий момент має диктувати формат, бо саме в ньому тонкий звіт залишить вас без опори. Для кожного перевіреного файлу зберігайте хеш вхідних даних, прапорці відповідності генератора та версію бібліотеки з рядка журналу вище, назву й версію валідатора, профіль, проти якого перевіряли, результат «пройшов/не пройшов» і ID провалених правил із номерами сторінок, де валідатор їх надає. Зберігайте цей звіт поруч із файлом, який він описує. Помістіть його в окрему систему — і цю систему виведуть з експлуатації раніше, ніж архів, який вона документує

Винятки теж потрібно записувати. Коли клієнт наполягає на відправленні файлу, який не подобається бар'єру, відповідь — не послаблювати правило для всіх. Запишіть, хто схвалив цей файл, на яких підставах і до якої дати, а потім прикріпіть це звільнення від вимог до звіту файлу. Звільнення від вимог з іменем і терміном дії — це рішення, за яке хтось відповідає. Перевірка, закоментована "тимчасово", — це інцидент, що чекає на свою дату

Ще одна звичка, яка окупається сама: коли файл провалюється, скопіюйте його в іменовану папку регресії, перш ніж хтось до нього торкнеться. Майже кожна проблема preflight, варта налагодження, веде до одного конкретного вхідного файлу, і команди, що зберігають ці вхідні дані, виправляють повторення за годину замість того, щоб чекати, поки воно знову спливе в продакшені. Показані тут властивості відповідності та Direct File API — частина HotPDF Delphi Component для Delphi та C++Builder, документація якого повністю висвітлює кожен виклик