Техническая статья

Преобразование гиперссылок в формате XFA Rich-Text в ссылки PDF в Delphi

XFA (XML Forms Architecture) упразднён. Стандарт ISO 32000-1 включает его в раздел §12.7 с пометкой об удалении из PDF 2.0, и современные просмотрщики один за другим отказываются от поддержки своих XFA-движков. Тем не менее архивы не опустели. Правительственные бланки, заявления на страхование и банковские выписки создавались в формате XFA на протяжении почти двух десятилетий - и эти файлы по-прежнему поступают в почтовые ящики и системы обработки документов. Когда просмотрщик, прежде умевший их отображать, перестаёт делать это, форма превращается в чистую страницу с заглушкой «откройте в другой программе». Надёжное решение - преобразовать XFA в статическое содержимое PDF, которое может отобразить любой просмотрщик

Сложная часть этого преобразования - не поля. Текстовые блоки и флажки достаточно чисто отображаются на виджеты AcroForm. Сложная часть - форматированный текст, который XFA хранит внутри элемента draw в блоке <exData contentType="text/html">. Этот блок представляет собой подмножество HTML со встроенными стилями и зачастую с якорями. Чтобы перенести его на страницу, необходимо воспроизвести как форматированный текст, так и активные гиперссылки - и именно на гиперссылках большинство реализаций тихо сдаётся

Как устроен форматированный текст XFA

Тело exData - небольшой фрагмент XHTML. Абзац - это <p>; стилизованный фрагмент символов - <span> с собственным встроенным CSS для жирности, курсива, цвета и размера; гиперссылка - это <a href="...">, обёртывающий видимый текст. Одна строка может содержать несколько тегов span подряд, каждый с разным оформлением, и один из них может быть якорем. Стилизация - не декоративный элемент, от которого можно избавиться. Условие, выделенное жирным красным шрифтом как юридическое предупреждение, обязано оставаться жирным и красным после преобразования, иначе преобразованный документ будет искажать оригинал

Таким образом, движок преобразования не может рассматривать блок как единую строку. Ему необходимо пройти по инлайновой структуре, восстановить эффективный стиль каждого фрагмента, наложив встроенный CSS тега span на базовый шрифт элемента draw, и разместить фрагменты последовательно вдоль строки. HotPDF моделирует каждый из этих размещённых фрагментов как внутреннюю запись TXFARichRun. Запись содержит текст фрагмента, его разрешённый стиль, измеренный блок и, для якоря, адрес Href

Размещение фрагментов слева направо

Позиционирование - это момент, когда работа с форматированным текстом из задачи разбора превращается в задачу вёрстки. Фрагменты занимают одну строку, поэтому каждый начинается там, где закончился предыдущий. В разметке нет данных, фиксирующих эти позиции; их необходимо измерить. Внутренняя процедура движка LayoutRichText измеряет каждый фрагмент теми же метриками шрифта, которыми затем будет отрисовывать его, а затем устанавливает горизонтальное смещение фрагмента равным накопленной сумме ширин всех предыдущих. Первый фрагмент начинается в точке начала координат блока draw, второй - на расстоянии ширины первого, третий - ширины первых двух, и так далее вдоль строки

Именно поэтому согласованность шрифта при измерении так важна. Проход разметки измеряет продвижения; отдельный проход рендеринга рисует глифы. Если эти два прохода расходятся в понимании шрифта, блоки, вычисленные разметкой, не будут совпадать с глифами, которые рисует рендерер. 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;

От якорного фрагмента к аннотации Link в PDF

Гиперссылка в готовом PDF не является частью содержимого страницы. Это отдельный объект - аннотация Link, описанная в ISO 32000-1 §12.5.6.5. Аннотация имеет /Rect, определяющий кликабельный прямоугольник на странице, и действие, срабатывающее при нажатии на прямоугольник. Для внешней ссылки действие является URI-действием: /S /URI с целевым адресом в виде строки /URI. Видимый текст под аннотацией - это обычное содержимое страницы; аннотация - невидимая горячая зона, наложенная поверх него

Путь преобразования точно следует этой модели. Когда фрагмент содержит Href, HotPDF сначала рисует стилизованный текст, а затем создаёт аннотацию Link над блоком фрагмента. Публичная точка входа для этой аннотации - метод страницы AddURILink, создающий объект /Type /Annot /Subtype /Link с URI-действием и возвращающий словарь аннотации. Его прямоугольник - измеренный блок фрагмента, преобразованный из локальных координат элемента draw в координаты страницы. Результат - ссылка, точно приходящаяся на текст якоря и нигде больше

// 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;

Почему область нажатия должна определяться через измеренные ширины

Заманчиво представить определение местоположения ссылки через поиск её видимого текста на странице с последующим обведением найденного прямоугольником. Это не работает, и причина фундаментальна для способа хранения преобразованного текста. Стилизованные фрагменты рисуются встроенными подмножественными шрифтами. Подмножественный шрифт перенумеровывает сохраняемые глифы, поэтому поток содержимого страницы содержит шестнадцатеричные CID-коды, а не исходные коды символов. Байты на странице - это не буквы, читаемые человеком, и они недоступны для поиска в качестве текста. Поиск подписи якоря ничего не находит, потому что эта подпись не существует как буквальный текст нигде в потоке

Единственный надёжный ориентир для прямоугольника - геометрия, уже вычисленная в ходе прохода разметки. Смещение и измеренная ширина каждого фрагмента были вычислены при формировании строки, до переноумерации каких-либо глифов, и описывают место, где текст физически появится. Поэтому HotPDF берёт прямоугольник ссылки непосредственно из блока размещённого фрагмента, а не из какого-либо текстового поиска. Поскольку при измерении использовался шрифт рендерера, блок корректен вне зависимости от подмножества. Геометрия переживает кодирование; текст - нет. Это весь аргумент в пользу позиционирования на основе измеренных ширин, и именно поэтому инструмент преобразования, пытающийся встроить ссылки через текстовый поиск, создаёт области нажатия, которые смещаются или исчезают

Управление преобразованием из вашего кода

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

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 после вызова. Список очищается в начале каждого преобразования и накапливает строку для каждого элемента, который движок не смог отобразить: неподдерживаемый тип поля, изображение draw, которое не удалось декодировать, блок exData без пригодных фрагментов. Ни одна из этих ситуаций не вызывает исключения, поэтому пустой список предупреждений свидетельствует о том, что всё было успешно обработано, а непустой точно указывает, какие оригиналы нужно проверить. Если вы работаете с необработанными XFA-данными в виде байтов XDP, а не с загруженным PDF, родственный метод ApplyXFAAsAcroForm принимает эти байты напрямую и использует тот же код обработки с тем же поведением предупреждений. Дополняющий метод AddXFAPacket работает в обратном направлении, встраивая XFA-пакет в документ, который вы создаёте

Проверка результата в просмотрщике

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

Преобразование самих полей - текстовых блоков, флажков и списков выбора, окружающих этот форматированный текст, - рассматривается в нашем руководстве по преобразованию XFA-форм в виджеты AcroForm. Об общей теме создания и размещения аннотаций Link вручную, помимо тех, что создаёт путь преобразования, см. работа с аннотациями PDF в HotPDF. Обе темы опираются на одну и ту же модель аннотаций и форм, входящую в состав HotPDF Component для Delphi и C++Builder