Шейпинг текста в компоненте 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