Формування тексту в компоненті PDFium йде через один встановлюваний об'єкт. ConfigureTextShaper встановлює формувальник, через який маршрутизується кожна точка входу формування, замінюючи та вивільняючи те, що було там; ActiveTextShaper повертає встановлений і творить платформовий типовий при першому вжитку; ActiveTextShaperName звітує, який бекенд живий; ClearTextShaper скидає встановлення і дозволяє типовому творитися знову. На Windows типовий — TPdfUniscribeTextShaper. Під Free Pascal є TPdfHarfBuzzTextShaper, який прив'язує libharfbuzz під час виконання, тож відсутня бібліотека — звітована умова, а не невдача завантаження
Один інтерфейс, два бекенди, що ділять роботу цілком по-різному. Розуміння тієї асиметрії — те, що заважає переносному шляхові творити текст, сформований правильно і позиційований неправильно
Чому бекенд Windows — один клас, а переносний — три частини?
Бо Uniscribe — чотири API, що прикидаються одним. ScriptItemize сегментує рядок за писемністю і розв'язує двонаправлені рівні; ScriptShape відображає символи в гліфи; ScriptPlace обчислює просування та зміщення; ScriptLayout кладе отримані пробіги у візуальний порядок. Бекенд, збудований на ній, тому не має чого додавати, — саме тому формувальник Windows є одним класом з одним методом
HarfBuzz покриває середні дві. Він формує та позиціонує пробіг, чий напрям і писемність викликач уже вирішив, і не має думки про те, як абзац розпадається на пробіги чи в якому порядку ті пробіги з'являються. Тож переносний бекенд постачає решту: двонаправлений алгоритм розв'язує рівні вкладення, функції Unicode HarfBuzz сегментують текст за писемністю, а пробіги кладуться у візуальний порядок, який творить правило L2 UAX #9. Двонаправлена половина достатньо велика, щоб бути власним модулем, описаним у статті про рівні вкладення UAX #9
Формувальник не розв'язує шрифти, і це навмисно
Uniscribe читає бінарник шрифта з контексту пристрою GDI. Переносного еквівалента тому немає, а вигадати один всередині модуля формування означало б вирішувати за кожен застосунок, звідки шрифти: з fontconfig, з CoreText, з теки шрифтів застосунку чи з бази даних. Тож бекенд HarfBuzz бере розв'язувача: зворотний виклик, що відображає назву шрифта в байти TrueType чи OpenType. Повернення False валить запит формування так само, як нечитабельний шрифт GDI валить його на Windows
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// Ваша політика: fontconfig, CoreText, тека шрифтів застосунку, база
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// Володіння переходить до модуля; викличте раз під час запуску,
// перш ніж що-небудь формуватиме текст
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// На Delphi платформовий типовий (Uniscribe) твориться на вимогу,
// тож встановлення взагалі не потрібне
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
Тримання пошуку шрифтів поза формувальником має другу вигоду, що проявляється на серверах: той самий процес може формувати з вбудованим набором шрифтів, що не має нічого спільного з уставленим на машині, — саме те, що потрібно, коли результат мусить бути відтворюваним байтово між хостами. Компонент також відкриває постачальника системних шрифтів хоста для випадків, коли вам справді потрібні уставлені шрифти, охоплений у статті про системного постачальника шрифтів
Запис результату нейтральний до бекенда, і кластери — причина
Обидва бекенди наповнюють той самий TPdfShapedText: вихідний текст, назву шрифта, розмір, байти шрифта, масив пробігів, загальну ширину, кількість гліфів та кількість логічних символів. Кожен TPdfShapedRun несе свій відрізок у вихідному тексті, візуальну позицію X, ширину, двонаправлений рівень і прапорець справа ліворуч, плюс свої гліфи. Кожен TPdfShapedGlyph несе ідентифікатор гліфа, просування, зміщення X і Y та кластер, до якого він належить, як початок і довжину у вихідному тексті
Ті поля кластерів — те, що робить запис придатним до вжитку, а не лише інформативним. Формування — не відображення один до одного: склад деванагарі стає одним гліфом із чотирьох символів, арабська лігатура зливає два, а один символ може дати кілька марок. Без відрізків кластерів ви не можете поставити каретку, перевірити влучання кліком чи підсвітити вибір, бо не можете сказати, яким символам належить гліф. З ними арифметика локальна, і той самий код працює для обох бекендів
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// Пробіги приходять уже у візуальному порядку із заповненим VisualX
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
Бюджети належать запису опцій
TPdfTextShapingOptions несе напрям плюс три стелі: максимум символів, максимум гліфів і максимум пробігів, з функцією класу Default, що заповнює розсудливі значення. Стелі — не параноя про спотворений вхід; це арифметика. Формування розгортається: шрифт з агресивною контекстною заміною може випромінювати більше гліфів, ніж вхідних символів, а абзац, що чергує писемності кожні кілька символів, творить пробіг на кожну зміну. Документ, зібраний максимізувати обидва, перетворює скромний рядок на великий розподіл, і служба, що формує текст із недовіриних PDF, потребує межу, яку вона обрала, а не межу, яку нав'язує машина
Встановлення напряму явно, замість лишення його на автоматі, варте робити щоразу, коли ви його вже знаєте. Автомат застосовує правила напряму абзацу, щоб вгадати з першого сильного символу, що правильно для вільного тексту і неправильно для поля форми, чий напрям — властивість поля, а не значення, яке хтось у нього набрав
Прив'язка під час виконання, а не залежність збірки
Бекенд HarfBuzz вантажить бібліотеку динамічно. Це рішення розгортання зі справжніми наслідками: один бінарник працює на машині з HarfBuzz і на машині без нього, звітуючи зменшені можливості в другому випадку замість відмови стартувати. Для бібліотеки, що постачається іншим розробникам, це єдиний робочий устрій, бо не можна вимагати від кожного споживача PDF-компонента здобувати та зістосовувати версії бібліотеки формування, яка йому може й не знадобитися
Відповідне правило для викликачів — перевіряти. ActiveTextShaper повертає nil, коли платформа не має типового і жодного не налаштовано, а точка входу формування звітує це як недоступний формувальник, а не як невдачу формування. Це різні проблеми і заслужують різних повідомлень: одна — прогалина розгортання, друга — проблема шрифта чи тексту
Встановіть раз, до того, як щось формуватиметься
Встановлення замінює та вивільняє попередній формувальник, тож повторний виклик безпечний, але безглуздий, а виклик, поки інший потік формує, взагалі не безпечний. Робіть це під час запуску. Якщо треба відкотитися на платформовий типовий пізніше, передайте nil — це також те, як ви скасовуєте тестовий двійник наприкінці тесту
Щойно бекенд встановлено, вимірювання та перенесення поводяться однаково на обох платформах, бо вони споживають метрики пробігів і гліфів, а не викликають платформу прямо; модель перенесення описана в статті про вимірювання тексту та перенесення слів. Підтримувані платформи та інструментарії компонента наведені на сторінці продукту PDFium Delphi component