HotPDF заполняет comb-поля AcroForm арабским, ивритом, диакритикой и смешанным по направлению текстом через THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Comb-сетка считается в скалярах Unicode, лигатура или базовая буква со своими знаками центрируется как один кластер на тех k ячейках, которыми владеют её скаляры, а вся строка прогоняется через алгоритм Unicode Bidirectional до того, как ячейки будут розданы
Каждое из трёх правил заменяет сокращение, которое отлично выглядит в латинской демке и рассыпается на первом настоящем бланке. Представьте государственную анкету с comb-полем из десяти ячеек под имя, написанное арабским. Заполните его позначной логикой — и получите буквы в изолированных формах, марширующие слева направо, лигатуру лам-алиф, втиснутую в одну ячейку при пустой соседней, и фатху, повисшую над неверной ячейкой. Каждый из этих дефектов — следствие счёта не той единицы или упорядочивания текста не в тот момент
Чего Comb-флаг на самом деле требует от PDF-писателя?
Comb-флаг велит писателю разбить поле ровно на MaxLen равноотстоящих позиций и положить в каждую по символу. ISO 32000-1 §12.7.4.3 определяет Comb как бит 25 в Table 228 — флаги полей, специфичные для текстовых полей, — и придаёт ему смысл только при наличии записи MaxLen из Table 229 и чистых флагах Multiline, Password и FileSelect. Сам бит — $1000000 в 32-битном значении /Ff; если вы когда-нибудь видели, как comb-поля сходят с ума после круга через переменную с плавающей точкой, статья о 32-битных флагах полей AcroForm объяснит, почему этот бит уходит первым
Спецификация говорит «символ» и останавливается. Она писалась под латинские боксы фиксированного шага и не говорит, что такое символ, когда пара арабских букв становится одним глифом или когда огласовка не имеет собственной ширины. HotPDF приходится выбирать единицу, и, выбирая, он строго проводит структурные правила: comb-поле, которое также multiline, или поле без положительного MaxLen, отвергается с диагностикой, а password-поля comb-полями быть не могут вовсе
Почему считать скаляры Unicode, а не единицы UTF-16, глифы или графемы?
HotPDF считает comb-ячейки в скалярах Unicode, потому что это та же единица, которой он проводит MaxLen, а сетка и проверка длины обязаны соглашаться — иначе значение, прошедшее валидацию, не влезет в свои ячейки. Три соблазнительные альтернативы ломают это согласие каждая по-своему
- Кодовые единицы UTF-16 — то, что возвращает Delphi-овский
Length, поэтому дополнительный символ вроде U+20BB7 съел бы две ячейки и два слота бюджета поля - Счёт глифов зависит от шрифта: большинство арабских шрифтов превращают лам плюс алиф в один глиф, поэтому число ячеек менялось бы от смены гарнитуры
- Счёт графем сливает базовую букву с её знаками, а значит сетка разошлась бы с проверкой
MaxLen, считающей каждый набранный пользователем скаляр
Валидация MaxLen в THPDFHeadlessDocument идёт по строке, только когда её UTF-16-длина превышает лимит, и тогда она считает валидную суррогатную пару за один раз и сразу отвергает непарную суррогат. Если ваш UI хочет предупреждать до вызова, а не ловить исключение после — считайте так же
// Считает скаляры Unicode так же, как comb-сетка:
// валидная суррогатная пара — один скаляр
function CountScalars(const S: UnicodeString): Integer;
var
I: Integer;
begin
Result := 0;
I := 1;
while I <= Length(S) do
begin
if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
(I < Length(S)) and
(Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
Inc(I, 2)
else
Inc(I);
Inc(Result);
end;
end;
Несколько значений показывают, насколько разъезжаются единицы и почему только колонка скаляров совпадает с тем, на что comb-поле размечает бюджет
| Значение | Единицы UTF-16 | Скаляры (ячейки) | Типичные глифы |
|---|---|---|---|
| Лам + алиф (لا) | 2 | 2 | 1 лигатура |
| Бе + фатха (بَ) | 2 | 2 | 2, знак прикреплён к базе |
| U+20BB7 (𠮷) | 2 | 1 | 1 |
| Разложенное e + U+0301 | 2 | 2 | 1 или 2, зависит от шрифта |
Как лигатуры и диакритика делят свои ячейки?
Шейпинговый кластер, покрывающий k скаляров, занимает k смежных ячеек и центрируется как единое целое на них. После шейпинга каждый кластер отображается назад на точный спан исходного текста, из которого он вышел, поэтому HotPDF знает, какими скалярами — а значит и какими ячейками — он владеет. Лигатура лам-алиф из примера выше получает две ячейки и сидит отцентрованной на обеих. Бе, несущая шадда и фатху, — три скаляра, поэтому получает три ячейки, со знаками, всё ещё прикреплёнными к букве
Центрирование — место, где ломается очевидная реализация. Центрирование каждого глифа в собственной ячейке отрывает знак от его базы, потому что позиция знака что-то значит только относительно буквы, которую он украшает. HotPDF держит каждый глиф кластера на его шейпинговом advance и смещениях X и Y, затем центрирует кластер по суммарному горизонтальному размаху естественного advance, позиционированных истоков глифов и реальных очертаний глифов. Кластер двигается блоком; внутри него ничего не переставляется
Одно следствие легко пропустить. Направляющие контролы вроде U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK и контролов embedding и isolate — скаляры Unicode без глифа. Они идут в счёт MaxLen и держат ячейку, которая останется пустой. Если исходные данные приходят из системы, подсыпающей эти знаки в имена, чистите их перед заполнением — иначе поле покажет дыру и начнёт отвергать значения на символ раньше, чем ожидает пользователь
Почему bidi-переупорядочивание обязано происходить до раздачи ячеек?
HotPDF сперва прогоняет всю строку через алгоритм Unicode Bidirectional (UAX #9) и только затем даёт каждому скаляру ячейку по его визуальной позиции. Раздача ячеек в логическом порядке с последующим переворотом результата работает на чисто арабской строке и падает, как только в строке появляются цифры или латинский токен: эти прогоны обязаны читать слева направо внутри строки справа налево. Значение вроде референс-кода с арабским префиксом и числом держит цифры в правильном порядке только если переупорядочивание видит всю строку разом. Обзор шейпинга complex script глубже раскрывает проблему логического и визуального порядка
Затем выравнивание размещает всю сетку занятых ячеек внутри поля. Унаследованная запись /Q поля побеждает, когда есть; Options.Alignment действует, только если у формы её нет. Для форм справа налево это существенно: короткое арабское значение в выровненном влево comb-поле заполнит левые ячейки — соответствующий спецификации и почти никогда не задуманный дизайнером бланка результат. Почините /Q формы или выставьте htaRight, когда собственного выравнивания у поля нет. Поворот виджета приходит из записи /MK /R, и при 90 или 270 градусах логический текстовый вьюпорт меняет ширину и высоту местами, тогда как логика ячеек остаётся прежней
Заполнение comb-поля смешанными шрифтами из Delphi
Comb-текст смешанными шрифтами использует одну сетку ячеек, один размер шрифта и одну базовую линию для всех поданных шрифтов. При FallbackFontFamilies равном True HotPDF подбирает шрифт для каждой полной графемы из поданных семейств в их настроенном порядке, поэтому арабская гарнитура может нести буквы, а латинская — встроенный ASCII. Каждый шрифтовой прогон шейпится с целой строкой в качестве контекста, знак никогда не отрывается от базы между шрифтами, и лигатура не выдумывается через границу шрифтов. Разложить каждый прогон по собственной локальной сетке или отмасштабировать его в собственный bounding box было бы проще — и сломало бы и позиции ячеек, и повёрнутые поля в момент, когда fallback переключает шрифт посреди строки
uses
System.SysUtils, System.IOUtils,
HPDFHeadlessDocument, HPDFHeadlessAppearance;
procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
Doc: THPDFHeadlessDocument;
Field: THPDFHeadlessFormField;
Families: THPDFHeadlessRichTextFontFamilies;
Options: THPDFHeadlessTextAppearanceOptions;
FieldIndex: Integer;
begin
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(SourceFile);
FieldIndex := Doc.FindFormField('applicant.name_ar');
if FieldIndex < 0 then
raise Exception.Create('Field not found');
Field := Doc.GetFormField(FieldIndex);
if (Field.Flags and $1000000) = 0 then // бит 25: Comb
raise Exception.Create('Not a comb field');
SetLength(Families, 2); // от одного до шестнадцати семейств
Families[0].FamilyName := 'Noto Naskh Arabic';
Families[0].Fonts[hrfsRegular] :=
TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
Families[1].FamilyName := 'Noto Sans';
Families[1].Fonts[hrfsRegular] :=
TFile.ReadAllBytes('NotoSans-Regular.ttf');
Options := THPDFHeadlessTextAppearanceOptions.Default;
Options.ShapeComplexText := True; // уже умолчание
Options.FallbackFontFamilies := True; // умолчание False
Options.FontSize := 0; // автоподбор, потолок 12
Options.Alignment := htaRight; // используется только без /Q
Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
Doc.SaveToFile(TargetFile); // должно отличаться от SourceFile
finally
Doc.Free;
end;
end;
Заметьте, чего пример не выставляет. При заполнении документа Width, Height, Rotation и CombCells выводятся из прямоугольника каждого виджета, его записи /MK и MaxLen поля, поэтому ваши значения в этих членах будут перезаписаны. Нулевой FontSize просит автоматический подбор, который сверяет размах каждого кластера с шириной его k ячеек и общей метрической и чернильной высотой всех участвующих шрифтов; фиксированный размер просто обрезается виджетом. CharacterSpacing и WordSpacing никогда не двигают comb-позиции: где приземлится кластер, решает одна сетка. Свои рантаймы HarfBuzz и FriBidi HotPDF грузит, только когда значение реально требует шейпинга, а значения без шейпинга оставляют существующую прямую comb-раскладку нетронутой. Общий конвейер заполнения загруженного PDF, включая права и сохранение, — в статье о задании значений полей формы в загруженном PDF
Чего шейпинговый comb-вид не гарантирует?
Шейпинговый comb-вид гарантирует верную раздачу ячеек, целые кластеры и сохранённое логическое значение; он не гарантирует бесшовные курсивные соединения и идеальное извлечение текста в каждом инструменте. Оба предела следуют из того, что такое comb-поле, и о них стоит знать, прежде чем обещать что-то клиенту
Первое — курсивные соединения через ячейки. Арабские буквы по-прежнему шейпятся с контекстом всей строки, поэтому каждая получает верную начальную, срединную или конечную форму, но фиксированный шаг ячеек кладёт пробел между отдельными кластерами, и соединительные штрихи не сомкнутся, как в бегущем тексте. Это врождённое свойство рисования соединительного письма в боксы, и никакое центрирование его не спрячет
Второе — извлечение текста. Запись /V поля хранит чистое логическое значение, а родительский Form XObject вида несёт один спан /ActualText (ISO 32000-1 §14.9.4) с полным вводом; дочерние Forms не несут ничего. Экстракторы, уважающие ActualText, восстановят исходную строку. Экстракторы, игнорирующие её, пересоберут текст по позициям глифов и могут вернуть другой порядок или продублировать текст кластера: у многоглифового кластера нет единственного очевидного значения Unicode на глиф. Если нижестоящей системе нужно значение, читайте поле, а не текст страницы
Оставшиеся границы механические. Comb-текст обязан быть одной строкой без табов, а значение с числом скаляров сверх числа ячеек падает, а не обрезается. Отказы транзакционны: отсутствующий глиф, невалидный шрифт, перерасход бюджета или отмена оставляют предыдущее значение и вид нетронутыми, поэтому отвергнутое заполнение никогда не бросает после себя полунаписанное поле
try
Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
on E: EHPDFHeadlessDocumentError do
if E.Code = hdLimitExceeded then
ShowFieldError('The value has more characters than the field allows')
else
raise;
on E: EHPDFHeadlessAppearanceError do
if E.Failure = haMissingGlyph then
ShowFieldError('None of the supplied fonts covers this text')
else
raise;
end;
// На любой из этих ошибок поле сохраняет прежнее значение и вид
Куда шейпинговые comb-поля вписываются в конвейер форм
Шейпинговая поддержка comb больше всего важна для форм, которые уже существуют: налоговые декларации, визовые анкеты, банковские onboarding-пакеты и регистрации по документу, спроектированные с клечатыми полями и теперь заполняемые арабским, ивритом, персидским или урду. Считайте в скалярах, храните исходное значение в /V, дайте одной сетке управлять всеми шрифтами и тестируйте значением, смешивающим буквы справа налево, цифры и диакритику — одна такая строка прогоняет каждое правило этой статьи. Описанные здесь headless API документа, шейпинг comb и fallback шрифтов — часть HotPDF Delphi PDF component