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

Перевірка відповідності PDF/A та PDF/UA у Delphi за допомогою PDF Library for Delphi

PDF/A і PDF/UA відповідають на два питання, які не мають між собою нічого спільного, і якщо ставитися до них як до одного прапорця «доступність і архівування», то саме так пошкоджені файли й потрапляють в архів під ярликом відповідності. PDF/A запитує, чи файл однаково точно відображатиметься й через двадцять років. PDF/UA запитує, чи допоміжні технології можуть прочитати його вже сьогодні. Документ може впевнено пройти одну перевірку й провалити іншу, тож єдиний чесний вердикт дає лише запуск обох, причому до того, як файл записано, а не після того, як система нижче за потоком повірить ідентифікатору відповідності, запеченому в його метаданих. Цей ідентифікатор — самодекларація. Ніщо у форматі не вимагає, щоб він був правдивим, і застосунок, який записує в XMP рядок «PDF/A-1b» без перевірки за стандартом, видає файл, що виглядає відповідним для будь-якого споживача, який читає лише цей ярлик. losLab PDF Library (PDF Library for Delphi) закриває цей розрив для Delphi та C++Builder, вбудовуючи обидва валідатори просто в бібліотеку, тож перевірка виконується в тому самому процесі й не потребує розгортання жодного зовнішнього сервісу

Два стандарти, що бракують файли з протилежних причин

ISO 19005 (PDF/A) — це контракт на відтворюваність. Відповідний файл мусить відображатися однаково й через десятиліття на програмному забезпеченні, яке ніколи не бачило системи, що його створила, тож правила атакують саме зовнішні залежності: усі шрифти вбудовано, колір прив'язано до вбудованого профілю ICC OutputIntent або виражено в незалежному від пристрою просторі, жодного шифрування в PDF/A-1, жодного JavaScript, метадані XMP узгоджуються зі словником інформації про документ. ISO 14289 (PDF/UA), натомість, — це контракт на семантику. Допоміжні технології мають пройти документ наскрізь і винести з нього сенс, який живе в геть іншому шарі: повне дерево структури, альтернативний текст для рисунків, заголовок документа, встановлений для показу, рівні заголовків без пропусків, зв'язки заголовків таблиць, що переживають момент, коли сторінка йде поза екран

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

Те, яку саме частину PDF/A ви обираєте за ціль, важить не менше, ніж сам факт відповідності. PDF/A-1 заморожено на рівні PDF 1.4, і він відкидає прозорість і JPEG2000 — те й те сучасний вивід звітності використовує не задумуючись. PDF/A-2 (ISO 19005-2, побудований на ISO 32000-1) приймає обидва і є розумним значенням за замовчуванням для нового архіву. PDF/A-3 іде ще далі й дозволяє вбудовані файли будь-якого типу, на що й спираються регульовані формати електронних рахунків. Команда, яка досі в 2026 році стандартизується на PDF/A-1b, зазвичай несе вимогу, яку хтось написав п'ятнадцять років тому, і перегляд цільової частини часто виходить дешевшим, ніж вирізання прозорості з кожної діаграми, яку видає система

Діаграма PDF Library for Delphi: контракт відтворення PDF/A проти контракту семантики PDF/UA плюс матриця проходження-провалу для документів, що задовольняють один і провалюють інший
PDF/A забезпечує вірний рендеринг на десятиліття вперед, а PDF/UA забезпечує допоміжне читання вже сьогодні, і вердикт одного не означає вердикт іншого

Структуровані зауваження на етапі прийому

Точка входу плаского API — CheckFileCompliance, де селектор тесту 1 означає PDF/A, а 2 — PDF/UA. Вона повертає дескриптор списку рядків, елементи якого — окремі зауваження, по одному на рядок, і саме така форма потрібна автоматизованому шлюзу для обходу:

function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0);  // 1 = PDF/A
  if ListId = 0 then
  begin
    // 0 означає «зауважень немає» АБО «файл нечитаний» — усуньте двозначність, перш ніж довіряти
    Result := Pdf.LastErrorCode = 0;
    Exit;
  end;
  for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
    LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
  Pdf.ReleaseStringList(ListId);
  Result := False;
end;

Два нюанси вирішують, чи запрацює це без нагляду. Перший — значення, що повертається, і воно означає дві протилежні речі. CheckFileCompliance повертає 0, коли файл повністю відповідає стандарту, а також коли файл узагалі не вдалося відкрити, бо внутрішньо порожній список результатів в обох випадках згортається до 0. Шлюз, що читає 0 як «пройдено», радо пропустить пошкоджені завантаження просто в архів, тож усувайте двозначність через LastErrorCode, перш ніж довіряти нулю, як і робить шлюз вище. Другий нюанс стосується того, на якому етапі життєвого циклу перебуває файл. Перевіряч працює на потоковому читачі бібліотеки замість повної моделі документа, відкриваючи файл напряму зі спільним доступом на читання й ніколи не викликаючи LoadFromFile, тому й може перемелювати вхідні дані в кілька гігабайтів, не будуючи дерево об'єктів. Те саме потокове відкриття провалюється, поки інший процес усе ще тримає файл на запис, а завантаження в процесі — це рівно такий стан. Ставте шлюз уже після завершення передавання

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

Звіти для людей, дифи для шлюзів збірки

Список зауважень — правильна форма для шлюзу і неправильна форма для листа команді шаблонів. CreatePreflightReport подає той самий аналіз у вигляді читабельної прози, CreatePreflightReportEx додає селектор формату звіту, а SavePreflightReport записує його на диск, тож звіт може подорожувати всередині пакета документів, що постачається. Чимало архівних контрактів роблять такий звіт окремим результатом поставки, а не просто внутрішнім артефактом

Учасник цієї родини, що тихо відпрацьовує своє місце, — це ComparePreflightReports. Відповідність — така сама поверхня для регресій, як і будь-яка інша поведінка. Правка шаблону, нещодавно ліцензований корпоративний шрифт чи оновлення бібліотеки — кожне з них може внести зауваження, якого не було в попередньому релізі, і жодне з них про це не попередить. Тримайте еталонні звіти для набору репрезентативних документів під контролем версій, перегенеровуйте їх після кожної зміни й запускайте ComparePreflightReports, щоб обчислити дельту. Порожній diff — це артефакт релізу, який варто зберігати. Несподіване зауваження провалює збірку, а це набагато дешевше місце для його виявлення, ніж аудит

Генерування виводу, що проходить з першого разу

Preflight відпрацьовує себе на файлах, що приходять звідкись іззовні. Для документів, які генерує ваш власний код, шукати порушення вже після генерації й латати їх заднім числом — повільний обхідний шлях. PDF Library for Delphi несе режим генерації для кожного стандарту, і обидва можна ввімкнути для одного й того самого документа:

Діаграма PDF Library for Delphi масштабованого приймання preflight: по одному потоковому екземпляру бібліотеки на робітника, шлюзовані відкриття після завершення завантажень і звільнені списки знахідок
Ворита відчиняються лише після завершення передавань, кожен робочий потік пропускає ввід read-only через приватний екземпляр бібліотеки, а кожен повернений хендл вимагає звільнення
var
  Pdf: TPDFlib;
  Diag: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);
    Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
    Pdf.SetPDFUAMode('en-US');
    Pdf.SetInformation(1, 'Quarterly Statement');  // /Title: обов'язково для PDF/UA
    // ... тут малюйте тегований вміст ...
    Diag := Pdf.GetPDFUADiagnostics;
    if Diag <> '' then
      Writeln('fix before shipping: ', Diag);
    Pdf.SaveToFile('statement.pdf');
    // preflight, що має значення, запускається на збереженому файлі:
    Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
  finally
    Pdf.Free;
  end;
end;

Пастка ховається саме в момент збереження. Кілька виправлень відповідності відбуваються під час серіалізації документа, а не тоді, коли ви вмикаєте режим: примусове виставлення прапорця друку в анотаціях, запис типового AFRelationship для вбудованих файлів PDF/A-3, нормалізація порядку обходу й описів полів форми для PDF/UA. Документ, що сидить у пам'яті, не є побайтово ідентичним тому, що потрапляє на диск, тож єдиний вердикт preflight, який щось означає, — це той, що обчислено за збереженим файлом. Перевіряйте сам statement.pdf. Не робіть висновків про відповідність з об'єкта, що досі в пам'яті, бо байти, які ви оцінюєте, — не ті байти, які ви відвантажили

Діаграма PDF Library for Delphi підправлень відповідності під час збереження, що застосовуються при серіалізації — саме тому preflight належить до збереженого PDF-файлу, а не до моделі в пам'яті
Серіалізація примусово вмикає прапорці друку анотацій, задає типовий AFRelationship і нормалізує порядок табуляції, тож інспекція пам'яті судить байти, які ніхто ніколи не відвантажує

Сценарії рахунків-фактур, що несуть машиночитаний XML поруч із візуальним документом, ідуть за шаблоном ZUGFeRD і Factur-X, побудованим на PDF/A-3. Для них варто явно виставити зв'язок вкладення через SetPDFA3DefaultAFRelationship, оскільки ISO 19005-3 вимагає, щоб кожен вбудований файл декларував свою роль відносно документа. Залиште це невстановленим — і вбудований XML стане просто блобом без заявленого призначення, а валідатор це помітить

Незалежні арбітри: veraPDF та Acrobat

Виробник не повинен бути єдиним суддею власного виводу. Перевіряльники PDF Library for Delphi дають швидкі, структуровані вердикти прямо в процесі, і це саме те, що потрібно на гарячому шляху, але шлюз релізу для архівної партії все одно має пропускати вивід через валідатор, якого ніхто у вашій команді не писав. veraPDF — еталонна реалізація для PDF/A, яку підтримує спільнота, і саме той інструмент, який найчастіше називають архіви у своїх критеріях прийняття, тож на нього й варто орієнтуватися. Профілі preflight в Acrobat — корисний третейський суддя, коли veraPDF і внутрішньопроцесна перевірка розходяться в думках. Фіксуйте назву валідатора та його версію поруч із кожним збереженим звітом. Твердження, що файл пройшов veraPDF, каже дуже мало без номера збірки, яка це підтвердила, бо інструмент посилює свої правила від релізу до релізу

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

Зашифрований вхід отримує скорочений шлях. Обидва перевіряльники приймають аргумент пароля, але файл PDF/A-1 зі словником шифрування вже не відповідає стандарту, бо ISO 19005-1 прямо забороняє шифрування, тож зашифроване подання можна відхилити ще до будь-якого глибшого аналізу. З'ясувати, що саме дозволяє словник шифрування, — окреме завдання, розкрите в статті аудит шифрування та прав доступу PDF

Зауваження PDF/UA майже завжди тягнуться до того, як спершу було складено дерево структури, а прийоми тегування, що стоять за цим, розкрито в статті побудова тегованих дерев структури PDF у Delphi. Архіви, які також вимагають цифрових підписів, мають поєднати цей шлюз із робочим процесом зі статті підписання та валідація PAdES. Повний довідник API preflight лежить на сторінці продукту losLab PDF Library for Delphi