Архитектура форм XML (XFA, XML Forms Architecture) признана устаревшей. Раздел §12.7 стандарта ISO 32000-1 содержит упоминание о ее удалении из спецификации PDF 2.0, и современные программы просмотра одна за другой прекращают поддержку XFA. Однако это не освободило архивы документов. Государственные формы отчетности, страховые заявления и банковские выписки создавались с использованием XFA на протяжении почти двух десятилетий, и эти файлы до сих пор поступают в информационные системы. Когда программа просмотра прекращает их поддержку, форма превращается в пустую страницу с заглушкой «пожалуйста, откройте документ в другом приложении». Надежным решением этой проблемы является сведение (flattening) XFA в статичное содержимое PDF, которое может отобразить любой ридер.
Сложность сведения заключается вовсе не в полях форм. Текстовые поля и переключатели довольно просто сопоставляются с виджетами AcroForm. Трудности вызывает форматированный текст (rich text), который XFA сохраняет внутри элемента рисования в блоке <exData contentType="text/html"> block. Этот блок представляет собой подмножество HTML со встроенными стилями и ссылками. Вывод этой информации на страницу требует воссоздания как стилизованного текста, так и активных гиперссылок, и именно на этапе работы с гиперссылками многие разработчики заходят в тупик.
Как устроен форматированный текст XFA
Содержимое exData представляет собой небольшой фрагмент XHTML. Абзац описывается тегом <p>; стилизованный фрагмент символов задается тегом <span> с собственными встроенными CSS-стилями для толщины, начертания, цвета и размера шрифта; а гиперссылка оформляется тегом <a href="...">, оборачивающим видимый текст. Одна строка может содержать несколько последовательных элементов span с разным оформлением, один из которых может быть ссылкой. Такое оформление нельзя просто удалить. Например, предупреждение, выделенное жирным красным шрифтом по юридическим причинам, должно остаться жирным и красным после сведения, иначе итоговый документ будет искажать исходный смысл.
По этой причине механизм сведения не может обрабатывать блок как единую строку. Ему необходимо обойти всю внутреннюю структуру, рассчитать итоговый стиль каждого фрагмента путем наложения встроенных стилей 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), описанный в разделе §12.5.6.5 стандарта ISO 32000-1. Аннотация имеет ключ /Rect, определяющий интерактивную прямоугольную область на странице, и действие, выполняемое при клике. Для внешних ссылок это действие URI (URI action): /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;
Почему интерактивная область должна рассчитываться по измеренной ширине
Может возникнуть искушение находить ссылки путем текстового поиска по странице и рисования прямоугольников вокруг найденных слов. Однако это не работает из-за особенностей хранения сведенного текста. Стилизованные фрагменты отрисовываются с использованием внедренных подмножеств шрифтов. Подмножество шрифта переопределяет коды используемых глифов, поэтому поток содержимого страницы содержит шестнадцатеричные коды 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 без корректных элементов span. Эти ситуации не вызывают исключений, поэтому пустой список предупреждений подтверждает успешность преобразования, а наличие записей указывает на проблемные элементы исходного документа. Если вы работаете с исходными байтами XFA (формат XDP), а не с готовым файлом PDF, метод ApplyXFAAsAcroForm принимает эти байты напрямую и задействует ту же логику обработки и генерации предупреждений. Взаимосвязанный метод AddXFAPacket выполняет обратную задачу, внедряя пакет XFA в создаваемый документ.
Проверка результатов в программе просмотра
Откройте сведенный файл в Acrobat или любой современной программе просмотра и убедитесь в двух вещах. Во-первых, форматированный текст должен отображаться корректно с сохранением стилей: жирные участки должны остаться жирными, цветные фрагменты должны сохранить свой цвет, а элементы должны располагаться последовательно в строке без наложений. Во-вторых, гиперссылки должны быть активными. При наведении на ссылку в строке состояния должен отображаться ее целевой адрес, а клик по ней должен приводить к переходу. Проверьте через инспектор аннотаций программы просмотра, что каждая ссылка представляет собой реальную аннотацию /Link, прямоугольник /Rect которой точно охватывает текст ссылки, находясь поверх статического контура символов, а не XFA-формы. Сочетание стилизованного статического текста и корректных аннотаций ссылок позволяет документу сохранять работоспособность без поддержки XFA.
Сведение самих полей форм (текстовых полей, переключателей и списков выбора), окружающих этот форматированный текст, описано в нашем руководстве по сведению форм XFA в виджеты AcroForm. Общие вопросы ручного создания и позиционирования аннотаций ссылок рассматриваются в статье о работе с аннотациями PDF в HotPDF. Обе технологии используют общую модель аннотаций и форм, поставляемую в составе HotPDF Component для Delphi и C++Builder.