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="...">, що обгортає видимий текст. Один рядок може містити кілька потоків підряд, кожен з різними стилями, і один з них може бути якорем. Стиль – це не прикраса, яку можна відкинути. Пункт, відтворений жирним червоним шрифтом, оскільки він є юридичним попередженням, повинен залишатися жирним і червоним після зведення, інакше зведений документ спотворює оригінал
Тому рушій зведення не може обробляти блок як один рядок. Він повинен обходити вбудовану структуру, вирішувати ефективний стиль кожного фрагмента шляхом накладення вбудованого 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;
Від серії якоря до анотації PDF Link
Гіперпосилання у готовому 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 з форматованим текстом, зі своїми стилізованими серіями та анотаціями посилань, виробляються в будь-якому разі. Функція повертає кількість видатих нею віджетів
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 або будь-якому сучасному переглядачі та перевірте дві речі. По-перше, форматований текст відтворено зі збереженим стилем: жирні серії є жирними, кольорові серії зберегли свій колір, і фрагменти розташовані в правильному порядку по рядку, а не перекривають один одного або виходять за межі блоку. По-друге, гіперпосилання є активними. Наведіть курсор на якір – рядок стану повинен показати цільову адресу; клацніть – URI-дія повинна відкрити її. Скористайтеся інспектором анотацій переглядача, щоб підтвердити, що кожна – це справжня анотація /Link, чий /Rect обіймає текст якоря, що лежить поверх вмісту, який тепер є простими намальованими гліфами, а не відтвореним XFA. Це поєднання – статичний стилізований текст плюс реальні анотації Link у правильних прямокутниках – робить зведений документ таким, що пережив XFA-рушії, які йому більше не потрібні
Зведення самих полів – текстових полів, прапорців та списків вибору, що оточують цей форматований текст – розглянуто в нашому покроковому посібнику зі зведення XFA-форм у віджети AcroForm. Для ширшої розповіді про побудову та розміщення анотацій Link вручну, поза тими, що генерує шлях зведення, дивіться роботу з анотаціями PDF у HotPDF. Обидва засновані на тій самій моделі анотацій та форм, що постачається разом з HotPDF Component для Delphi та C++Builder