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

Файлові асоціації рівня сторінки в PDF 2.0 з PDFlibPas

PDFlibPas прикріплює вбудований файл до однієї конкретної сторінки, а не до документа загалом: масив /AF записується в словник сторінки, тоді як сам вантаж лишається зареєстрованим у дереві імен EmbeddedFiles документа. Саме цей розподіл описує ISO 32000-2 §14.13, і саме він дозволяє читачеві відповісти на питання, на яке вкладення рівня документа відповісти не може: до якої сторінки належать ці дані

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

Один вантаж, два місця, звідки на нього посилаються

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

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

Структура асоційованого файлу рівня сторінки в документі PDF 2.0, записаному PDFlibPas: вантаж вбудовується один раз і реєструється в дереві імен EmbeddedFiles під каталогом документа, тоді як словник сторінки несе масив /AF, що посилається на ту саму файлову специфікацію з ключем AFRelationship, тож ClearPageAssociatedFiles від'єднує прив'язку, не знищуючи даних
Асоціація рівня сторінки додає друге посилання, а не другу копію: читачі, які знають лише вкладення рівня документа, все одно знаходять вантаж у дереві імен, а зняття прив'язки сторінки лишає вбудований потік досяжним

У цієї функції є один навмисно вузький критерій успіху, який варто знати. Вона повідомляє успіх лише тоді, коли сторінка справді мала ключ /AF. Сторінка, яка ніколи не мала асоціацій, повертає відмову, а не веселу констатацію, тож викликаючий не сплутає no-op із завершеним очищенням

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // Кріпимо ряд вимірів, з якого збудовано графік на сторінці 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // файл на диску
      'measurements.csv',         // ім'я для показу всередині PDF
      'text/csv',                 // MIME-тип
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship, ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

Рядок відношення на практиці не є вільним текстом. ISO 32000-2 визначає словник — Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema і Unspecified, — і споживачі орієнтуються на нього. Data для чисел за графіком, Source для документа, з якого згенеровано сторінку, Alternative для еквівалентного подання. Обирайте зі словника, навіть якщо поки ніщо у вашому конвеєрі це не читає, бо наступний інструмент у ланцюжку може читати

Чому тому самому пошуку потрібен FollowRef в обидва боки?

Бо робота з посиланнями відповідає на два різні питання, і код мусить знати, яке саме він ставить. Пошук за ключем, який слідує непрямим посиланням, повертає об'єкт, на який посилання вказує. Пошук без слідування повертає саме посилання. Обидва коректні, і використання невідповідного дає німу неправильну поведінку, а не помилку

Читання асоційованого файлу демонструє перший напрям. Щоб отримати номер об'єкта вбудованого потоку за ключами /EF і /F файлової специфікації, пошук має не слідувати, бо слідування розв'язує посилання в об'єкт потоку, і номер об'єкта зникає. Правило узагальнюється: будь-який кодовий шлях, якому потрібна ідентичність об'єкта, а не його вміст, мусить брати сире посилання

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

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

Карта рішень для слідування посиланнями в PDF-пошуках, як це реалізовано в PDFlibPas: читання /EF і /F під файловою специфікацією не має слідувати посиланню, бо відповіддю є номер об'єкта вбудованого потоку, тоді як непрямий словник /OCProperties у каталозі треба розв'язувати, інакше провалена перевірка типу мовчки перезапише наявну конфігурацію опційного вмісту
Той самий пошук відповідає на два різні питання: для ідентичності потрібне сире посилання, для вмісту — розв'язаний об'єкт, а гола перевірка типу замість цього рішення рано чи пізно виконує неправильну гілку, нічого не кидаючи
// Вкладення рівня документа та асоціації рівня сторінки співіснують.
// Вбудований файл можна позначити асоційованим і на рівні документа
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// Очищення від'єднує прив'язку сторінки; вантаж лишається в дереві імен
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

Що режими відповідності роблять зі вкладеннями

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

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

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

Коли асоціація має бути на сторінку, а не на документ?

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

Підтримка — практичне обмеження. Асоційовані файли рівня сторінки — конструкція PDF 2.0, і підтримка в переглядачах тонша, ніж у вкладень рівня документа. Оскільки вантаж у будь-якому разі сидить у дереві імен, переглядач, який ігнорує /AF на сторінках, усе одно покаже файл у списку вкладень, тож деградація м'яка. Але якщо прив'язка до сторінки для вашого споживача суттєва, а не просто корисні метадані, перевіряйте того читача, під який ви справді цілитеся, а не припускайте

Асоційовані файли рівня сторінки, вкладення рівня документа й архівні профільні ворота, які керують обома, постачаються в бібліотеці PDFlibPas для Delphi. Якщо ви попутно ще й лагодите старіші файли на вході, то робота з метаданими та відповідністю в конвертації в PDF/A з ремонтом метаданих — це те, що вирішує, який із цих маршрутів вкладень вам узагалі доступний