Artigo Técnico

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

O shaping de texto no componente PDFium passa por um único objeto instalável. O ConfigureTextShaper instala o shaper por que todos os pontos de entrada de shaping passam, substituindo e libertando o que lá estava; o ActiveTextShaper devolve o instalado e cria o padrão da plataforma à primeira utilização; o ActiveTextShaperName reporta que backend está ativo; o ClearTextShaper abandona a instalação e deixa o padrão ser criado de novo. No Windows o padrão é TPdfUniscribeTextShaper. Sob Free Pascal existe o TPdfHarfBuzzTextShaper, que vincula a libharfbuzz em tempo de execução, pelo que uma biblioteca em falta é uma condição reportada em vez de uma falha de carregamento

Arquitetura de shaping de texto plugável no componente PDFium Delphi: ConfigureTextShaper, ActiveTextShaper e ClearTextShaper gerem um backend instalado, Uniscribe no Windows e um HarfBuzz vinculado em execução sob Free Pascal
Cada chamada de shaping passa 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. Compreender essa assimetria é o que impede o caminho portátil de produzir texto que é moldado corretamente e posicionado erradamente

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

Porque o Uniscribe são quatro APIs a fazer-se passar por uma. O ScriptItemize segmenta uma string por sistema de escrita e resolve níveis bidirecionais; o ScriptShape mapeia caracteres para glifos; o ScriptPlace calcula avanços e desvios; o ScriptLayout põe os runs resultantes em ordem visual. Um backend construído sobre ele portanto não tem nada a acrescentar, que é porque o shaper Windows é uma única classe com um único método

O HarfBuzz cobre as duas do meio. Molda e posiciona um run cuja direção e sistema de escrita o chamador já decidiu, e não tem opinião sobre como um parágrafo se divide em runs nem sobre que ordem esses runs aparecem. Por isso o backend portátil fornece o resto: o algoritmo bidirecional resolve níveis de embutimento, as funções Unicode do HarfBuzz segmentam o texto por sistema de escrita, 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 a sua própria unidade, descrita no artigo sobre níveis de embutimento UAX #9

Comparação de pipelines 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 volta das etapas próprias UAX #9 do componente
O Uniscribe cobre as quatro etapas; o caminho portátil tem de fornecer ele próprio a itemização e a ordem visual

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

O Uniscribe lê o binário da fonte a partir de um contexto de dispositivo GDI. Não há equivalente portátil disso, e inventar um dentro de uma unidade de shaping significaria decidir, em nome de todas as aplicações, se as fontes vêm do fontconfig, do CoreText, de uma pasta de fontes da aplicação, ou de uma base de dados. Por isso o backend HarfBuzz recebe um resolvedor: um callback que mapeia um nome de fonte para os bytes TrueType ou OpenType. Devolver False faz o pedido de shaping falhar da mesma forma que uma fonte GDI ilegível o faz falhar no Windows

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

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // A sua política: fontconfig, CoreText, uma pasta de fontes da app, uma base 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; chamar uma vez durante o arranque,
  // antes de qualquer coisa moldar texto
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // No Delphi o padrão da plataforma (Uniscribe) é criado a pedido,
  // pelo que não é precisa instalação nenhuma
  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 moldar com um conjunto de fontes embutidas que nada tem a ver com o que está instalado na máquina, que é o que se quer quando a saída tem de ser reprodutível byte a byte entre anfitriões. O componente também expõe um fornecedor de fontes do sistema anfitrião para os casos em que quer mesmo fontes instaladas, coberto no artigo sobre o fornecedor de fontes do sistema

O registo de resultado é neutro de backend, e os 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 transporta o seu segmento no texto de origem, a sua posição X visual, a sua largura, o seu nível bidirecional e uma flag da direita para a esquerda, mais os seus glifos. Cada TPdfShapedGlyph transporta um identificador de glifo, um avanço, desvios 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 registo utilizável em vez de meramente informativo. O shaping não é um mapeamento um a um: uma sílaba Devanagari torna-se um glifo a partir de quatro caracteres, uma ligadura árabe funde dois, e um único carácter pode produzir várias marcas. Sem segmentos de cluster não se pode colocar um cursor, testar um clique por colisão, ou destacar uma seleção, porque não se pode dizer a que caracteres um glifo pertence. Com eles, a aritmética é local e o mesmo código funciona para ambos os backends

Segmentos de cluster de glifos no TPdfShapedText: um glifo de sílaba Devanagari a partir de quatro caracteres, uma ligadura árabe de dois, e uma base mais marca de um carácter, cada um mapeado de volta através de ClusterStart e ClusterLength
Os segmentos de cluster mapeiam cada glifo de volta aos seus caracteres de origem para que cursores, testes de colisão 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;

Os orçamentos pertencem ao registo de opções

O TPdfTextShapingOptions transporta uma direção mais três limites: 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 limites não são paranoia sobre entrada malformada; são aritmética. O shaping expande: uma fonte com substituição contextual agressiva pode emitir mais glifos do que caracteres de entrada, e um parágrafo que alterne sistemas de escrita a cada poucos caracteres produz um run por alternância. Um documento montado para maximizar ambos transforma uma string modesta numa alocação grande, e um serviço que molda 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 a deixar em automático vale a pena fazer sempre que já a conhece. O automático aplica as regras de direção de parágrafo para adivinhar a partir do primeiro carácter forte, o que está certo para texto livre e errado para um campo de formulário cuja direção é uma propriedade do campo e não do valor que alguém escreveu nele

Vinculação em execução, não uma dependência de compilação

O backend HarfBuzz carrega a biblioteca dinamicamente. Essa é uma decisão de implantação com consequências reais: um binário corre numa máquina com HarfBuzz e numa máquina sem ele, reportando capacidade reduzida no segundo caso em vez de falhar ao arrancar. Para uma biblioteca distribuída a outros programadores esse é o único arranjo viável, porque não se pode exigir a todos os consumidores de um componente PDF que adquiram e façam corresponder versões de uma biblioteca de shaping de que talvez não precisem

A regra correspondente para os chamadores é verificar. O ActiveTextShaper devolve 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 é um buraco de implantação, o outro é um problema de fonte ou de texto

Instalar uma vez, antes de qualquer coisa moldar

A instalação substitui e liberta o shaper anterior, pelo que chamá-la repetidamente é seguro mas inútil, e chamá-la enquanto outra thread molda não é seguro de todo. Faça-o durante o arranque. Se precisar de recuar ao padrão da plataforma mais tarde, passe nil, que é também como desfaz um duplo de teste no fim de um teste

Uma vez instalado um backend, a medição e a quebra de linhas comportam-se da mesma forma em ambas as plataformas, já que consomem as métricas de runs e glifos em vez de chamar a plataforma diretamente; o modelo de quebra de linhas está descrito no artigo sobre medição de texto e quebra de palavras. As plataformas e toolchains suportadas para o componente estão listadas na página de produto do PDFium Delphi component