Текстовото оформяне в PDFium компонента минава през един включваем обект; ConfigureTextShaper инсталира shaper-а, през който всяка shaping входна точка се насочва, замествайки и освобождавайки каквото е било там; ActiveTextShaper връща инсталирания и създава платформения по подразбиране при първа употреба; ActiveTextShaperName докладва кой бекенд е жив; ClearTextShaper сваля инсталацията и позволява по подразбиране да бъде създаден отново; На Windows по подразбиране е TPdfUniscribeTextShaper; Под Free Pascal има TPdfHarfBuzzTextShaper, който обвързва libharfbuzz по време на изпълнение, така че липсваща библиотека е докладвано състояние, а не отказ при зареждане
Един интерфейс, два бекенда, които разделят работата съвсем различно; Разбирането на тази асиметрия е това, което спира портабилния път да произвежда текст, който е оформен правилно и позициониран погрешно
Защо 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 нива
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-тестнете клик или да маркирате селекция, защото не можете да кажете на кои символи принадлежи глиф; С тях аритметиката е локална и същият код работи за двата бекенда
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