Artigo Técnico

Shaping de texto plugável: Uniscribe e HarfBuzz no Delphi

O shaping de texto no componente PDFium passa por um objeto instalável. O ConfigureTextShaper instala o shaper pelo qual todo ponto de entrada de shaping encaminha, substituindo e liberando o que estava lá; o ActiveTextShaper retorna o instalado e cria o padrão da plataforma no primeiro uso; o ActiveTextShaperName reporta qual backend está ativo; o ClearTextShaper descarta a instalação e permite que o padrão seja criado de novo. No Windows o padrão é o TPdfUniscribeTextShaper. Sob Free Pascal há o TPdfHarfBuzzTextShaper, que vincula a libharfbuzz em tempo de execução para que uma biblioteca ausente seja uma condição reportada em vez de uma falha de carga

Arquitetura de shaping de texto plugável no componente PDFium Delphi: ConfigureTextShaper, ActiveTextShaper e ClearTextShaper gerenciam um backend instalado, Uniscribe no Windows e um HarfBuzz vinculado em runtime sob Free Pascal
Cada chamada de shaping é encaminhada pelo único objeto shaper instalado, com um padrão de plataforma em cada alvo

Uma interface, dois backends que dividem o trabalho de forma completamente diferente. Entender essa assimetria é o que impede o caminho portátil de produzir texto com shaping correto e posicionamento errado

Por que o backend Windows é uma classe e o portátil três peças?

Porque o Uniscribe são quatro APIs fingindo ser uma. O ScriptItemize segmenta uma string por script e resolve níveis bidirecionais; o ScriptShape mapeia caracteres para glifos; o ScriptPlace calcula advances e offsets; o ScriptLayout põe os runs resultantes em ordem visual. Um backend construído sobre ele portanto não tem nada a acrescentar, que é por que o shaper do Windows é uma única classe com um único método

O HarfBuzz cobre as duas do meio. Ele dá shaping e posiciona um run cuja direção e script o chamador já decidiu, e não tem opinião sobre como um parágrafo se divide em runs ou em que ordem esses runs aparecem. Então o backend portátil fornece o resto: o algoritmo bidirecional resolve níveis de embedding, as funções Unicode do HarfBuzz segmentam o texto por script, e os runs são dispostos na ordem visual que a regra L2 do UAX #9 produz. A metade bidirecional é substancial o suficiente para ser sua própria unidade, descrita em o artigo de níveis de embedding do UAX #9

Comparação de pipeline de shaping para texto PDF: o Uniscribe fornece ScriptItemize, ScriptShape, ScriptPlace e ScriptLayout dentro de uma classe, enquanto o HarfBuzz cobre só shaping e posicionamento em torno dos estágios próprios de UAX #9 do componente
O Uniscribe cobre os quatro estágios; o caminho portátil precisa fornecer itemização e ordem visual ele mesmo

O shaper não resolve fontes, e isso é deliberado

O Uniscribe lê o binário da fonte de um device context GDI. Não há equivalente portátil disso, e inventar um dentro de uma unidade de shaping significaria decidir, em nome de todo aplicativo, se as fontes vêm do fontconfig, do CoreText, de uma pasta de fontes do aplicativo ou de um banco de dados. Então o backend HarfBuzz recebe um resolver: um callback que mapeia um nome de fonte aos bytes TrueType ou OpenType. Retornar False falha o pedido de shaping do mesmo jeito que uma fonte GDI ilegível o falha no Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Sua política: fontconfig, CoreText, uma pasta de fontes do app, um banco de dados
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // A posse passa para a unidade; chame uma vez durante a inicialização,
  // antes de qualquer coisa dar shaping a texto
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // No Delphi o padrão da plataforma (Uniscribe) é criado sob demanda,
  // então nenhuma instalação é necessária de forma alguma
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Manter a descoberta de fontes fora do shaper tem um segundo benefício que aparece em servidores: o mesmo processo pode dar shaping com um conjunto de fontes embutidas que não tem nada a ver com o que está instalado na máquina, que é o que você quer quando a saída precisa ser reprodutível byte a byte entre hosts. O componente também expõe um provedor de fontes do sistema hospedeiro para os casos em que você quer fontes instaladas, coberto em o artigo do provedor de fontes do sistema

O record de resultado é neutro de backend, e clusters são a razão

Ambos os backends preenchem o mesmo TPdfShapedText: o texto de origem, nome da fonte, tamanho, bytes da fonte, um array de runs, a largura total, a contagem de glifos e a contagem de caracteres lógicos. Cada TPdfShapedRun carrega seu trecho no texto de origem, sua posição X visual, sua largura, seu nível bidirecional e um flag da direita para a esquerda, mais seus glifos. Cada TPdfShapedGlyph carrega um identificador de glifo, um advance, offsets X e Y, e o cluster a que pertence como um início e um comprimento no texto de origem

Esses campos de cluster são o que tornam o record utilizável em vez de meramente informativo. Shaping não é um mapeamento um para um: uma sílaba Devanagari vira um glifo a partir de quatro caracteres, uma ligadura árabe funde dois, e um único caractere pode produzir várias marcas. Sem trechos de cluster você não pode posicionar um caret, testar um clique, ou destacar uma seleção, porque não pode dizer a quais caracteres um glifo pertence. Com eles, a aritmética é local e o mesmo código funciona para ambos os backends

Trechos de cluster de glifos no TPdfShapedText: um glifo de sílaba Devanagari a partir de quatro caracteres, uma ligadura árabe a partir de dois, e uma base mais marca a partir de um caractere, cada um mapeado de volta por ClusterStart e ClusterLength
Trechos de cluster mapeiam cada glifo de volta a seus caracteres de origem para que carets, hit tests e seleções funcionem
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
      // Os runs já chegam em ordem visual com VisualX preenchido
      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;

Orçamentos pertencem ao record de opções

O TPdfTextShapingOptions carrega uma direção mais três tetos: máximo de caracteres, máximo de glifos e máximo de runs, com uma função de classe Default que preenche valores sensatos. Os tetos não são paranoia com entrada malformada; são aritmética. Shaping expande: uma fonte com substituição contextual agressiva pode emitir mais glifos que caracteres de entrada, e um parágrafo que alterna scripts a cada poucos caracteres produz um run por troca. Um documento montado para maximizar ambos transforma uma string modesta em uma alocação grande, e um serviço que dá shaping a texto de PDFs não confiáveis precisa de um limite que escolheu em vez de um limite que a máquina impõe

Definir a direção explicitamente em vez de deixá-la no automático vale a pena sempre que você já a conhece. O automático aplica as regras de direção de parágrafo para adivinhar a partir do primeiro caractere forte, o que está certo para texto livre e errado para um campo de formulário cuja direção é uma propriedade do campo em vez do valor que alguém digitou nele

Vinculação em runtime, não uma dependência de build

O backend HarfBuzz carrega a biblioteca dinamicamente. Essa é uma decisão de implantação com consequências reais: um binário roda em uma máquina com HarfBuzz e em uma sem ele, reportando capacidade reduzida no segundo caso em vez de falhar ao iniciar. Para uma biblioteca distribuída a outros desenvolvedores esse é o único arranjo viável, porque você não pode exigir que todo consumidor de um componente PDF adquira e iguale a versão de uma biblioteca de shaping de que talvez não precise

A regra correspondente para os chamadores é verificar. O ActiveTextShaper retorna nil quando a plataforma não tem padrão e nenhum foi configurado, e o ponto de entrada de shaping reporta isso como um shaper indisponível em vez de uma falha de shaping. São problemas diferentes e merecem mensagens diferentes: um é uma lacuna de implantação, o outro é um problema de fonte ou texto

Instale uma vez, antes de qualquer coisa dar shaping

A instalação substitui e libera o shaper anterior, então chamá-la repetidamente é seguro mas inútil, e chamá-la enquanto outra thread está dando shaping não é seguro de forma alguma. Faça isso durante a inicialização. Se precisar recuar ao padrão da plataforma depois, passe nil, que também é como você desfaz um dublê de teste no fim de um teste

Uma vez que um backend está instalado, medição e quebra de linha se comportam igual nas duas plataformas, já que consomem as métricas de run e glifo em vez de chamar a plataforma diretamente; o modelo de quebra de linha é descrito em o artigo de medição de texto e word wrap. As plataformas e toolchains suportadas pelo componente estão listadas na página de produto do PDFium Delphi component