Технічна стаття

Змінне формування тексту: Uniscribe та HarfBuzz у Delphi

Формування тексту в компоненті PDFium йде через один встановлюваний об'єкт. ConfigureTextShaper встановлює формувальник, через який маршрутизується кожна точка входу формування, замінюючи та вивільняючи те, що було там; ActiveTextShaper повертає встановлений і творить платформовий типовий при першому вжитку; ActiveTextShaperName звітує, який бекенд живий; ClearTextShaper скидає встановлення і дозволяє типовому творитися знову. На Windows типовий — TPdfUniscribeTextShaper. Під Free Pascal є TPdfHarfBuzzTextShaper, який прив'язує libharfbuzz під час виконання, тож відсутня бібліотека — звітована умова, а не невдача завантаження

Архітектура змінного формування тексту в компоненті PDFium Delphi: ConfigureTextShaper, ActiveTextShaper і ClearTextShaper керують одним встановленим бекендом, Uniscribe на Windows і прив'язаний під час виконання HarfBuzz під Free Pascal
Кожен виклик формування маршрутизується через єдиний встановлений об'єкт формувальника, з платформовим типовим на кожній цілі

Один інтерфейс, два бекенди, що ділять роботу цілком по-різному. Розуміння тієї асиметрії — те, що заважає переносному шляхові творити текст, сформований правильно і позиційований неправильно

Чому бекенд Windows — один клас, а переносний — три частини?

Бо Uniscribe — чотири API, що прикидаються одним. ScriptItemize сегментує рядок за писемністю і розв'язує двонаправлені рівні; ScriptShape відображає символи в гліфи; ScriptPlace обчислює просування та зміщення; ScriptLayout кладе отримані пробіги у візуальний порядок. Бекенд, збудований на ній, тому не має чого додавати, — саме тому формувальник Windows є одним класом з одним методом

HarfBuzz покриває середні дві. Він формує та позиціонує пробіг, чий напрям і писемність викликач уже вирішив, і не має думки про те, як абзац розпадається на пробіги чи в якому порядку ті пробіги з'являються. Тож переносний бекенд постачає решту: двонаправлений алгоритм розв'язує рівні вкладення, функції Unicode HarfBuzz сегментують текст за писемністю, а пробіги кладуться у візуальний порядок, який творить правило L2 UAX #9. Двонаправлена половина достатньо велика, щоб бути власним модулем, описаним у статті про рівні вкладення UAX #9

Порівняння конвеєра формування для тексту PDF: Uniscribe постачає ScriptItemize, ScriptShape, ScriptPlace і ScriptLayout в одному класі, тоді як HarfBuzz покриває лише формування та позиціювання навколо власних стадій UAX #9 компонента
Uniscribe покриває всі чотири стадії; переносний шлях мусить сам постачати поділ на елементи та візуальний порядок

Формувальник не розв'язує шрифти, і це навмисно

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 та кластер, до якого він належить, як початок і довжину у вихідному тексті

Ті поля кластерів — те, що робить запис придатним до вжитку, а не лише інформативним. Формування — не відображення один до одного: склад деванагарі стає одним гліфом із чотирьох символів, арабська лігатура зливає два, а один символ може дати кілька марок. Без відрізків кластерів ви не можете поставити каретку, перевірити влучання кліком чи підсвітити вибір, бо не можете сказати, яким символам належить гліф. З ними арифметика локальна, і той самий код працює для обох бекендів

Відрізки кластерів гліфів у TPdfShapedText: один гліф складу деванагарі з чотирьох символів, арабська лігатура з двох і основа з маркою з одного символу, кожне відображене назад через ClusterStart і ClusterLength
Відрізки кластерів відображають кожен гліф назад у його вихідні символи, тож каретки, перевірки влучання та вибори працюють
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