Техническа статия

Включваемо текстово оформяне: Uniscribe и HarfBuzz в Delphi

Текстовото оформяне в PDFium компонента минава през един включваем обект; ConfigureTextShaper инсталира shaper-а, през който всяка shaping входна точка се насочва, замествайки и освобождавайки каквото е било там; ActiveTextShaper връща инсталирания и създава платформения по подразбиране при първа употреба; ActiveTextShaperName докладва кой бекенд е жив; ClearTextShaper сваля инсталацията и позволява по подразбиране да бъде създаден отново; На Windows по подразбиране е TPdfUniscribeTextShaper; Под Free Pascal има TPdfHarfBuzzTextShaper, който обвързва libharfbuzz по време на изпълнение, така че липсваща библиотека е докладвано състояние, а не отказ при зареждане

Архитектура на включваемо текстово оформяне в PDFium Delphi компонента: ConfigureTextShaper, ActiveTextShaper и ClearTextShaper управляват един инсталиран бекенд, Uniscribe на Windows и run-time обвързан HarfBuzz под Free Pascal
Всяко shaping извикване се насочва през единствения инсталиран shaper обект, с платформено подразбиране на всяка цел

Един интерфейс, два бекенда, които разделят работата съвсем различно; Разбирането на тази асиметрия е това, което спира портабилния път да произвежда текст, който е оформен правилно и позициониран погрешно

Защо Windows бекендът е един клас, а портабилният три части?

Защото Uniscribe е четири API, престорени като един; ScriptItemize сегментира низ по скрипт и резолва bidirectional нива; ScriptShape съпоставя символи към глифи; ScriptPlace изчислява advances и offsets; ScriptLayout слага получените runs във визуален ред; Бекенд, изграден върху него, следователно няма нищо останало за добавяне, което е причината Windows shaper-ът да е единствен клас с единствен метод

HarfBuzz покрива средните две; Той оформя и поставя run, чиято посока и скрипт извикващият вече е решил и няма мнение как абзац се разделя на runs или в какъв ред тези runs се появяват; Така че портабилният бекенд доставя останалото: bidirectional алгоритъмът резолва embedding нива, HarfBuzz Unicode функциите сегментират текста по скрипт и runs са подредени във визуалния ред, който правилото L2 на UAX #9 произвежда; Bidirectional половината е съществена достатъчно, за да бъде собствен модул, описан в статията за UAX #9 embedding нива

Сравнение на shaping конвейер за PDF текст: Uniscribe доставя ScriptItemize, ScriptShape, ScriptPlace и ScriptLayout вътре в един клас, докато HarfBuzz покрива само оформяне и поставяне около собствените UAX #9 етапи на компонента
Uniscribe покрива и четирите етапа; портабилният път трябва сам да доставя itemization и визуален ред

Shaper-ът не резолва шрифтове и това е нарочно

Uniscribe чете шрифтовия двоичен файл от GDI device context; Няма портабилен еквивалент на това и измислянето на такъв вътре в shaping модул би означавало да се реши, от името на всяко приложение, откъде идват шрифтовете — от fontconfig, от CoreText, от папка с шрифтове на приложението или от база данни; Така че HarfBuzz бекендът приема resolver: callback, който съпоставя име на шрифт към TrueType или OpenType байтовете; Връщането на False проваля shaping заявката по същия начин, по който нечетим 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;

Държането на откриването на шрифтове извън shaper-а има втора полза, която се проявява в сървъри: същият процес може да оформя с вграден шрифтов набор, който няма нищо общо с инсталираното на машината, което е това, което искате, когато изходът трябва да бъде байт-възпроизводим между хостове; Компонентът също излага доставчик на шрифтове на хост системата за случаите, в които наистина искате инсталирани шрифтове, разгледан в статията за системния font provider

Записът резултат е бекенд-неутрален и clusters са причината

И двата бекенда запълват един и същ TPdfShapedText: source текста, име на шрифт, размер, шрифтови байтове, масив от runs, общата ширина, броя глифи и броя логически символи; Всеки TPdfShapedRun носи своята отсечка в source текста, своята визуална X позиция, своята ширина, своето bidirectional ниво и флаг отдясно-наляво, плюс своите глифи; Всеки TPdfShapedGlyph носи глиф идентификатор, advance, X и Y отмествания и cluster-а, на който принадлежи, като начало и дължина в source текста

Тези cluster полета са това, което прави записа употребяем, а не само информативен; Оформянето не е едно-към-едно съпоставяне: деванагарска сричка става един глиф от четири символа, арабска лигатура слива два, а един символ може да произведе няколко знаци; Без cluster отсечки не можете да поставите каретка, да hit-тестнете клик или да маркирате селекция, защото не можете да кажете на кои символи принадлежи глиф; С тях аритметиката е локална и същият код работи за двата бекенда

Glyph cluster отсечки в TPdfShapedText: един глиф от деванагарска сричка от четири символа, арабска лигатура от два и основа плюс знак от един символ, всеки съпоставен обратно през ClusterStart и ClusterLength
Cluster отсечките съпоставят всеки глиф обратно към неговите source символи, така че каретки, hit тестове и селекции работят
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
      // Runs вече пристигат във визуален ред с 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 носи посока плюс три тавана: максимум символи, максимум глифи и максимум runs, с class функция Default, която запълва разумни стойности; Таваните не са параноя за недобре оформен вход; те са аритметика; Оформянето се разширява: шрифт с агресивна контекстна замяна може да излъчи повече глифи от входните символи, а абзац, който редува скриптове на всеки няколко символа, произвежда run на превключване; Документ, сглобен да максимизира и двете, превръща скромен низ в голямо разпределение и услуга, която оформя текст от ненадеждни PDF, се нуждае от лимит, който тя е избрала, а не лимит, който машината налага

Задаването на посоката изрично, вместо да я оставите на автоматично, си заслужава, когато вече я знаете; Автоматичното прилага правилата за посока на абзац, за да гадае от първия силен символ, което е право за свободен текст и грешно за form поле, чиято посока е свойство на полето, а не на стойността, която някой е въвел в него

Обвързване по време на изпълнение, не build зависимост

HarfBuzz бекендът зарежда библиотеката динамично; Това е deployment решение с реални последици: един двоичен файл тича на машина с HarfBuzz и на машина без него, докладвайки намалена възможност във втория случай, вместо да се провали да стартира; За библиотека, доставяна на други разработчици, това е единственото работещо уреждане, защото не можете да изисквате всеки консуматор на PDF компонент да придобие и съвпадне по версия shaping библиотека, от която може да не се нуждае

Съответното правило за извикващите е да проверяват; ActiveTextShaper връща nil, когато платформата няма по подразбиране и не е конфигуриран никой, а shaping входната точка докладва това като недостъпен shaper, а не като shaping провал; Това са различни проблеми и заслужават различни съобщения: едното е deployment дупка, другото е шрифтов или текстов проблем

Инсталирайте веднъж, преди каквото и да е оформя

Инсталацията замества и освобождава предишния shaper, така че извикването ѝ многократно е безопасно, но безсмислено, а извикването ѝ, докато друг thread оформя, изобщо не е безопасно; Направете го по време на стартиране; Ако трябва да паднете обратно към платформеното по подразбиране по-късно, подайте nil, което е и начинът да отмените тестов дубъл в края на тест

Щом бекенд е инсталиран, измерването и пренасянето се държат еднакво и на двете платформи, тъй като те консумират run и glyph метриките, вместо да викат платформата директно; моделът за пренасяне е описан в статията за измерване на текст и пренасяне на думи; Поддържаните платформи и toolchain-и за компонента са изброени на продуктовата страница PDFium Delphi component