Technical Article

Alternativas Estilísticas GSUB OpenType em Delphi Puro

Um designer escolhe uma fonte com um a de andar único para cabeçalhos, ou um zero cortado para tabelas, ou um conjunto de capitais enfeitadas (swash) para uma capa. Esses glifos já estão na fonte. Eles simplesmente não são os padrões. O a padrão é mapeado do caractere por meio da tabela cmap para um ID de glifo, e a alternativa fica a alguns glifos de distância, acessível apenas por meio de uma regra de substituição. Produzir essa alternativa em um PDF significa ler a regra e emitir o glifo substituto no fluxo de conteúdo. Este artigo trata da leitura dessas regras, especificamente o tipo de substituição simples, em Object Pascal, sem biblioteca de modelagem (shaping) nativa por baixo.

O escopo é estreito de propósito. Conjuntos e alternativas estilísticas são substituições do tipo um glifo de entrada, um glifo de saída. Eles são a parte do layout OpenType que você pode resolver com uma caminhada de tabela simples e determinística, o que os torna muito adequados para um motor Pascal que deseja permanecer livre de dependências em C.

Por que Delphi puro em vez do HarfBuzz

O HarfBuzz é a resposta óbvia para "modelar este texto", e para modelagem completa bidirecional, índica ou árabe, ele é a resposta correta. Ele também é uma biblioteca C. Vinculá-lo a um produto Delphi ou C++Builder significa distribuir um binário nativo para cada plataforma e arquitetura de destino, corresponder à sua convenção de chamada, acompanhar sua cadência de lançamento e ler seus termos de licença em relação aos seus. Nada disso é difícil isoladamente. No entanto, tudo isso representa um atrito constante e não agrega valor quando o requisito real é apenas "forneça-me a forma ss01 desta letra".

A substituição simples não precisa de um motor de modelagem. Ela precisa de um analisador (parser) para alguns formatos de subtabela GSUB e uma busca binária ou duas. Escrever isso em Pascal mantém toda a cadeia de ferramentas dentro de um único compilador. O limite real é que essa abordagem lida com consultas de substituição de glifos e nada mais. Não é resolução bidirecional, não é reordenação índica e não é modelagem contextual automática. Onde esses recursos são necessários, eles são realmente indispensáveis, e uma consulta de substituição simples não os substituirá.

A hierarquia GSUB, de cima a baixo

A tabela de Substituição de Glifos (GSUB) é organizada como uma cadeia de indireções, e uma consulta de substituição percorre a cadeia a partir do topo. No topo está a ScriptList. Uma tag de script como latn seleciona uma entrada, e a tag especial DFLT é o script padrão que se aplica quando nenhum script mais específico corresponde. A entrada do script aponta para uma LangSys, o sistema de idiomas, com uma LangSys padrão para o caso comum e outras nomeadas opcionais para idiomas que precisam de comportamentos diferentes. O turco é o exemplo comum, onde o i com ponto e sem ponto exige tratamento específico.

A LangSys nomeia um conjunto de índices de recursos. Cada índice aponta para la FeatureList, onde um registro de recurso carrega uma tag de quatro bytes, como ss01, e uma lista de índices de pesquisa (lookup). Esses índices finalmente apontam para a LookupList, onde as subtabelas de substituição reais residem. Portanto, resolver ss01 significa: localizar o script, localizar sua LangSys, localizar o recurso cuja tag é ss01, coletar as pesquisas que ele indica e aplicá-las. O HotPDF usa como padrão o script DFLT e a LangSys padrão, que é o que a grande maioria dos designs de texto em alfabeto latino adota, e expõe uma maneira de substituir a tag de script quando uma fonte vincula seus recursos a um script específico.

Tabelas de cobertura (Coverage) decidem quem participa

Toda subtabela de substituição começa com a mesma pergunta: este glifo de entrada participa desta regra e, em caso afirmativo, onde ele se situa na indexação da própria regra. Essa pergunta é respondida por uma tabela de cobertura (Coverage table), e a resposta é um índice de cobertura, um pequeno número ordinal que o restante da subtabela usa para pesquisar em que o glifo se transforma.

A cobertura vem em dois formatos. O Formato 1 é a lista de IDs de glifos ordenados de forma crescente. Você localiza um glifo com uma busca binária, e sua posição na lista é seu índice de cobertura. O Formato 2 é uma lista de registros de intervalo, onde cada um contém um glifo inicial, um glifo final e o índice de cobertura ao qual o glifo inicial é mapeado. Um glifo dentro de um intervalo obtém seu índice de cobertura calculando o deslocamento a partir do início do intervalo. O Formato 1 é compacto quando os glifos participantes estão espalhados; o Formato 2, quando eles formam sequências contíguas. Ambos são ordenados, de modo que ambos são pesquisados em tempo logarítmico, e ambos retornam um índice de cobertura ou um sinal nítido de "não coberto", permitindo que o motor deixe o glifo inalterado.

Substituição Simples, os dois formatos

A Substituição Simples é o LookupType 1 e mapeia um glifo para exatamente um substituto. Ela também possui dois formatos, e la divisão é uma otimização de espaço. O Formato 1 armazena um delta sinalizado único. O ID do glifo de saída é o ID do glifo de entrada mais esse delta, módulo 65536. É assim que uma fonte codifica uma substituição em que cada glifo participante fica a um deslocamento fixo de sua alternativa, por exemplo, um bloco de numerais de altura total (lining figures) posicionado a uma distância constante dos numerais de estilo antigo (oldstyle figures) correspondentes. A tabela de cobertura indica quais glifos se qualificam, e o delta único atende a todos eles.

O Formato 2 armazena um array explícito de IDs de glifos substitutos. O índice de cobertura da tabela de cobertura é o índice nesse array, de modo que o glifo no índice de cobertura 0 torna-se a primeira entrada do array, o índice de cobertura 1 a segunda, e assim por diante. O Formato 2 é usado quando as alternativas não estão sob um deslocamento uniforme, que é o caso comum para conjuntos estilísticos criados manualmente. A consulta é idêntica para quem a chama, em ambos os casos. Receba o glifo de entrada, execute-o pela cobertura e, se ele estiver coberto, aplique o delta ou leia a posição do array.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

O contrato que merece atenção é o repasse (pass-through). GetSingleSubstituteGlyph retorna o ID do glifo de entrada inalterado em caso de erro ou ausência: sem fonte, sem tabela GSUB, sem recurso correspondente ou sem cobertura encontrada. Isso significa que a chamada é segura para ser feita de forma incondicional. Você solicita a alternativa e, se ela não existir, recebe de volta exatamente o que inseriu, de forma que o código chamador nunca precisa tratar de forma especial uma fonte que careça do recurso.

O que significam as tags de recursos estilísticos

A tag de recurso é o vocabulário de qual alternativa você está solicitando, e as tags de interesse para o trabalho estilístico resumem-se a uma lista curta. O par principal é salt, alternativas estilísticas (stylistic alternates), que dá acesso geral às formas alternativas de um glifo, e de ss01 a ss20, os vinte conjuntos estilísticos numerados que uma fonte pode definir, onde cada um representa um grupo nomeado de substituições que o designer reuniu. Uma fonte pode colocar um a de andar único e um R de perna reta sob a tag ss03, por exemplo, de modo que ativar esse único conjunto reestiliza ambos.

Em torno deles residem várias outras tags de substituição simples. aalt representa o acesso a todas as alternativas (access-all-alternates), a união de cada alternativa que um glifo possui, geralmente apresentada como um recurso de paleta de glifos. titl seleciona capitais de títulos desenhadas para tamanhos grandes. subs e sups substituem os numerais padrão por índices e expoentes reais, em vez de versões apenas reduzidas. ordn produz formas ordinais, as letras suspensas em 1º e 2º. frac constrói frações, embora frações diagonais completas também dependam de lógicas de ligadura e de contexto que vão além da substituição simples. Para os casos de glifo único, o mecanismo é idêntico a ss01: passe a tag para a consulta de substituição e leia o glifo alternativo retornado.

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

Formato cmap 12 e os planos suplementares

Antes que qualquer substituição possa ser executada, um caractere deve se transformar em um glifo, e esse é o papel da tabela cmap. O caminho de consulta de substituição começa sempre do caractere para o glifo por meio da tabela cmap, e depois do glifo para a alternativa por meio da GSUB. A parte interessante da tabela cmap é o seu alcance. Uma subtabela de formato 4 cobre o Plano Multilíngue Básico (BMP), que são os primeiros 65.536 pontos de código, o que basta para a maior parte dos textos em alfabeto latino. Mas não é suficiente para pontos de código de U+10000 para cima, os planos suplementares, onde residem atualmente os símbolos alfanuméricos matemáticos, diversos ícones e vários sistemas de escrita em uso.

Format 12 is the subtable that covers the full U+0000 to U+10FFFF range. Trata-se de uma lista ordenada de grupos, onde cada grupo contém um ponto de código inicial, um ponto de código final e um ID de glifo inicial, de modo que uma sequência contígua de pontos de código é mapeada para uma sequência contígua de glifos. O HotPDF resolve pontos de código com uma estratégia híbrida que corresponde à forma como os dados são organizados. Pontos de código no BMP são atendidos a partir de um array direto indexado pelo ponto de código, uma busca simples sem necessidade de pesquisa. Pontos de código nos planos suplementares são atendidos a partir de uma tabela esparsa ordenada pelo ponto de código e pesquisada por meio de busca binária. O resultado é que GetUnicodeGlyphForCodepoint recebe um Cardinal completo e responde corretamente em todo o intervalo, retornando o ID de glifo 0, o glifo .notdef, para qualquer ponto de código que a fonte não mapeie.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

Onde essas consultas param

As APIs de substituição simples respondem a um tipo específico de pergunta, e é bom deixar claro o que elas não respondem. O LookupType 1 é apenas um dos oito tipos de substituição. A consulta não gerencia a substituição múltipla (LookupType 2), na qual um glifo se transforma em vários, nem a substituição de ligadura (LookupType 4), na qual vários glifos se transformam em um. Também não gerencia os tipos contextuais e contextuais encadeados (LookupTypes 5 e 6), que são acionados apenas quando um glifo aparece em uma determinada vizinhança, nem os tipos de extensão e encadeamento reverso. Uma fração diagonal, um caractere conjunto do devanágari ou uma cascata árabe de inicial-média-final constituem problemas de sequência, e uma busca simples por glifo não consegue expressá-los.

A solução também não executa modelagem automática. Nada aqui inspeciona uma sequência de texto, decide quais recursos ativar e os aplica na ordem exigida pelo sistema de escrita. O chamador escolhe a tag do recurso e a aplica glifo por glifo. Essa é exatamente a ferramenta correta para conjuntos estilísticos e alternativas, que são opcionais e locais, e a ferramenta incorreta para um sistema de escrita que exige reordenação. Manter a fronteira bem definida é o que permite que o caminho de substituição permaneça pequeno e previsível.

Para os casos que exigem trabalho no nível de sequência, a modelagem de scripts complexos é abordada em nosso artigo sobre modelagem de texto de escrita complexa no Delphi. Se as suas substituições fazem parte de um trabalho de relatório maior que também posiciona imagens e outras fontes na página, o guia para saída de relatórios com fontes e imagens mostra como essas peças se integram. Todas elas funcionam no mesmo motor, o HotPDF Component para Delphi e C++Builder, que traz as consultas de substituição GSUB juntamente com a incorporação de fontes, subconjuntos e as APIs de texto cobertas em outras partes deste blog.