Technical Article

Зведення гіперпосилань форматованого тексту XFA до посилань PDF у Delphi

XFA, архітектура XML-форм, є застарілою. Стандарт ISO 32000-1 згадує її в параграфі §12.7 із приміткою про те, що її вилучено з версії PDF 2.0, і сучасні засоби перегляду один за одним відмовляються від двигунів XFA. Проте це не звільнило архіви. Державні форми подачі документів, страхові заяви та банківські виписки створювалися у форматі XFA протягом майже двох років, і ці файли досі надходять на електронні скриньки та в системи документообігу. Коли засіб перегляду, який раніше відображав їх, перестає це робити, форма перетворюється на порожню сторінку з повідомленням 'будь ласка, відкрийте в іншій програмі читання'. Надійним виправленням є зведення (flattening) XFA у статичний вміст PDF, який може намалювати будь-яка програма читання.

Найскладнішою частиною цього зведення є не поля. Текстові поля та прапорці відображаються у віджети AcroForm досить просто. Складність полягає у форматованому тексті (rich text), який XFA зберігає всередині елемента малювання в блоці <exData contentType="text/html">. Цей блок є підмножиною HTML із вбудованими стилями та часто з посиланнями (анкорами). Перенесення його на сторінку означає відтворення як стилізованого тексту, так і активних гіперпосилань, і саме на гіперпосиланнях більшість реалізацій тихо здаються.

Як насправді виглядає форматований текст XFA

Вміст exData — це невелика частина XHTML. Абзац — це тег <p>; стилізований рядок символів — це тег <span> із власним вбудованим CSS для насиченості, накреслення, кольору та розміру; а гіперпосилання — це тег <a href="...">, який обгортає видимий текст. Один рядок може містити кілька елементів span поспіль, кожен з яких має різний стиль, і один із них може бути посиланням. Стилізація — це не просто декорація, яку можна відкинути. Пункт, виділений жирним червоним кольором через юридичне попередження, має залишатися жирним і червоним після зведення, інакше зведений документ не відповідатиме оригіналу.

Тому двигун зведення не може обробляти блок як один рядок. Він повинен пройтися по вбудованій структурі, визначити результуючий стиль кожного фрагмента шляхом накладання CSS-стилів span на базовий шрифт елемента малювання і розташувати фрагменти один за одним уздовж рядка. HotPDF моделює кожен із цих розташованих фрагментів як внутрішній запис TXFARichRun. Запис містить текст фрагмента, його визначений стиль, його виміряні межі та, для посилання, адресу Href, на яку воно вказує.

Розміщення фрагментів зліва направо

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

Цей є причиною узгодження шрифтів при вимірюванні має таке велике значення. Етап компонування вимірює зміщення; окремий етап рендерингу малює гліфи. Якщо ці два етапи не узгоджені щодо шрифту, межі, обчислені під час компонування, не збігатимуться з гліфами, які малює візуалізатор. HotPDF тримає їх узгодженими, відображаючи визначений стиль кожного фрагмента на специфікацію шрифту через внутрішній помічник RunStyleToFontSpec, який відповідає власним налаштуванням візуалізатора за замовчуванням: Arial розміром 10 пунктів. Виміряне зміщення та намальований текст тоді узгоджуються між собою, а обчислена область фрагмента дійсно покриває символи, які бачить користувач.

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

Від фрагмента анкора до анотації посилання PDF

Гіперпосилання в готовому PDF не є частиною вмісту сторінки. Це окремий об'єкт — анотація посилання (Link annotation), опис якої наведено в ISO 32000-1 §12.5.6.5. Анотація має поле /Rect, яке визначає прямокутник на сторінці для кліку, та дію, яка запускається при натисканні на прямокутник. Для зовнішнього посилання це дія URI: /S /URI з цільовою адресою у вигляді рядка /URI. Видимий текст під ним є звичайним вмістом сторінки; анотація є просто невидимою активною зоною, накладеною поверх нього.

Процес зведення діє саме за цією моделлю. Коли фрагмент містить Href, HotPDF спочатку малює стилізований текст, а потім будує анотацію посилання поверх області цього фрагмента. Публічною точкою входу для цієї анотації є метод сторінки AddURILink, який створює об'єкт /Type /Annot /Subtype /Link з дією /URI та повертає словник анотації. Його прямокутник — це виміряна область фрагмента, переведена з локальних координат елемента малювання в координати сторінки. Результатом є посилання, яке розташовується точно поверх тексту посилання і ніде більше.

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Чому область кліку має залежати від виміряної ширини

Може виникнути спокуса шукати посилання на сторінці за його видимим текстом і малювати прямокутник навколо знайденого. Але це не працює, і причина полягає в тому, як саме зберігається зведений текст. Стилізовані фрагменти малюються за допомогою вбудованих субшрифтів (subset fonts). Субшрифт перенумеровує гліфи, які він містить, тому потік вмісту сторінки зберігає шістнадцяткові коди CID, а не оригінальні коди символів. Байти на сторінці — це не ті літери, які читає людина, і їх неможливо шукати як текст. Пошук назви посилання нічого не знайде, оскільки ця назва не існує як текстовий рядок ніде в потоці.

Єдиним надійним орієнтиром для прямокутника є геометрія, яку етап компонування вже обчислив. Зміщення кожного фрагмента та виміряна ширина були розраховані під час побудови рядка, ще до того, як будь-який гліф був перенумерований, і вони описують, де текст з'явиться фізично. Тому HotPDF бере прямокутник посилання безпосередньо з виміряної області фрагмента, а не за допомогою пошуку тексту. Оскільки вимірювання використовувало шрифт рендерингу, область буде правильною незалежно від субсетування. Геометрія переживає кодування; текст — ні. Це головний аргумент на користь позиціонування на основі виміряної ширини, і саме тому спроби знайти посилання пошуком тексту призводять до зсуву або зникнення активних зон.

Керування зведенням з вашого коду

Для PDF, який уже містить пакет XFA, точкою входу є метод FlattenLoadedXFA. Завантажте документ, викличте метод та збережіть результат. Параметр Editable визначає, що відбувається з полями форми: передайте True, щоб залишити їх як віджети AcroForm для заповнення, або False, щоб зробити кожен віджет доступним лише для читання, перетворюючи вихідний документ на заморожений запис. Блоки малювання форматованого тексту з їхніми стилізованими фрагментами та анотаціями посилань створюються в обох випадках. Функція повертає кількість виведених віджетів.

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Завжди зчитуйте XFAFlattenWarnings після виклику. Список очищується на початку кожного зведення та накопичує рядки для кожного елемента, який двигун відмовився візуалізувати: непідтримуваний тип поля, зображення малювання, яке не вдалося декодувати, або блок exData без придатних фрагментів. Жоден із цих випадків не викликає винятку, тому порожній список попереджень є доказом того, що все було успішно перенесено, а непорожній список точно вказує, які оригінальні елементи слід перевірити. Коли ви маєте справу з вихідними даними XFA у вигляді байтів XDP, а не із завантаженим PDF, суміжний метод ApplyXFAAsAcroForm приймає ці байти безпосередньо та використовує той самий шлях коду і ту саму логіку попереджень. Додатковий метод AddXFAPacket діє у зворотному напрямку, вбудовуючи пакет XFA в документ, який ви створюєте.

Підтвердження результату в програмі перегляду

Відкрийте зведений файл в Acrobat або будь-якій сучасній програмі перегляду та перевірте дві речі. По-перше, чи відображається форматований текст із збереженням його стилю: жирні фрагменти залишилися жирними, кольорові фрагменти мають свій колір, а елементи span розташовані в правильному порядку в рядку без накладання або виходу за межі області. По-друге, чи активні гіперпосилання. Наведіть курсор на посилання, і рядок стану має показати цільову адресу; натисніть на нього, і дія URI повинна відкрити її. Використовуйте інспектор анотацій програми перегляду, щоб підтвердити, що кожне посилання є справжньою анотацією /Link, чиє поле /Rect охоплює текст посилання, розташовуючись поверх вмісту, який тепер є звичайними намальованими гліфами, а не згенерованим формою XFA. Це поєднання стилізованого статичного тексту та справжніх анотацій Link на правильних прямокутниках дозволяє зведеному документу існувати набагато довше за двигуни XFA, які йому більше не потрібні.

Зведення самих полів (текстових полів, прапорців та списків вибору), які оточують цей форматований текст, опис яких наведено в нашій інструкції зі зведення форм XFA у віджети AcroForm. Детальніше про створення та розміщення анотацій Link вручну (крім тих, які створюються автоматично при зведенні) див. у статті Робота з анотаціями PDF у HotPDF. Обидва рішення базуються на тій самій моделі анотацій та форм, яка постачається у складі HotPDF Component для Delphi та C++Builder.