Um designer escolhe uma fonte com um a de um único nível para cabeçalhos, ou um zero cortado para tabelas, ou um conjunto de maiúsculas ornamentadas (swash) para uma capa. Esses glifos já estão na fonte. Simplesmente não são a opção predefinida. O a predefinido mapeia-se a partir do carácter através da tabela cmap para um glifo, e a alternativa situa-se a alguns IDs de glifo de distância, alcançável apenas através de uma regra de substituição. Produzir essa alternativa num PDF implica ler a regra e emitir o glifo substituto no fluxo de conteúdo. Este artigo aborda a leitura dessas regras, do tipo de substituição única, em Object Pascal sem qualquer biblioteca nativa de modelação (shaping) subjacente.
O âmbito é estreito de propósito. Os conjuntos e alternativas estilísticas são substituições de entrada de glifo único e saída de glifo único. Constituem a parte do esquema OpenType que pode resolver com um pequeno percurso determinístico na tabela, o que os torna muito adequados para um motor Pascal que pretenda permanecer livre de dependências de C.
Porquê Delphi puro em vez do HarfBuzz
O HarfBuzz é a resposta óbvia para "shape this text", e para modelações bidirecionais complexas, como sistemas de escrita índicos ou árabes, é a resposta correta. Contudo, é também uma biblioteca C. Integrá-la num produto Delphi ou C++Builder obriga ao fornecimento de um objeto nativo para cada plataforma e arquitetura de destino, à correspondência da sua convenção de chamada, ao acompanhamento do seu ritmo de lançamentos e à leitura dos termos da sua licença face à sua própria. Nada disso é difícil isoladamente. Mas representa uma fricção constante que nunca desaparece, e não traz qualquer vantagem quando o requisito real é apenas "fornecer a forma ss01 desta letra".
A substituição única não necessita de um motor de modelação. Precisa de um analisador (parser) para um punhado de formatos de subtabelas GSUB e de uma ou duas pesquisas binárias. Escrever isso em Pascal mantém toda a cadeia de ferramentas dentro de um único compilador. O limite honesto é que esta abordagem lida com consultas de substituição de glifos e nada mais. Não realiza resolução bidirecional (bidi), não efetua reordenação de caracteres índicos e não faz modelação contextual automática. Onde estas operações são necessárias, são de facto indispensáveis, e uma consulta de substituição única não as substituirá.
A hierarquia GSUB, de cima a baixo
A tabela GSUB está organizada como uma cadeia de indireções, e uma consulta de substituição percorre a cadeia a partir do topo. No topo encontra-se a ScriptList. Uma etiqueta (tag) de script, como latn, seleciona uma entrada, e a etiqueta especial DFLT representa o script predefinido aplicado quando nenhum script mais específico corresponde. A entrada de script aponta para um LangSys, o sistema de idioma, com um LangSys predefinido para o caso comum e opções nomeadas para idiomas que necessitam de comportamentos diferentes. O turco é o exemplo habitual, onde o i com ponto e sem ponto exigem um tratamento próprio.
O LangSys identifica um conjunto de índices de funcionalidades (features). Cada índice aponta para a FeatureList, onde um registo de funcionalidade contém uma etiqueta de quatro bytes, incluindo a ss01, e uma lista de índices de consulta. Esses índices apontam, finalmente, para a LookupList, onde residem as subtabelas de substituição reais. Assim, resolver ss01 significa: encontrar o script, encontrar o seu LangSys, encontrar a funcionalidade cuja etiqueta é ss01, recolher as consultas por ela nomeadas e aplicá-las. HotPDF utiliza por predefinição o script DFLT e o LangSys padrão, que é o formato em que a grande maioria dos designs de texto latino é distribuída, e expõe uma forma de sobrepor a etiqueta de script quando uma fonte associa as suas funcionalidades sob um script específico.
As tabelas de cobertura decidem quem participa
As tabelas de cobertura decidem quem participa
Cada subtabela de substituição começa com a mesma questão: este glifo de entrada participa nesta regra e, em caso afirmativo, qual a sua posição no índice da própria regra. Essa questão é respondida por uma tabela de Cobertura (Coverage table), e a resposta é um índice de cobertura, um pequeno ordinal que o resto da subtabela utiliza para procurar em que glifo se converte.
A cobertura apresenta-se em dois formatos. O Formato 1 é uma lista de IDs de glifo ordenados por ordem crescente. Localiza um glifo com uma pesquisa binária, e a sua posição na lista é o seu índice de cobertura. O Formato 2 é uma lista de registos de intervalos, contendo cada um um glifo inicial, um glifo final e o índice de cobertura ao qual o glifo inicial se mapeia. Um glifo dentro de um intervalo obtém o seu índice de cobertura aplicando um desvio a partir do início do intervalo. O Formato 1 é compacto quando os glifos participantes estão dispersos; o Formato 2 é preferível quando se agrupam em sequências contínuas. Ambos são ordenados, pelo que ambos são pesquisados em tempo logarítmico, e ambos devolvem um índice de cobertura ou uma indicação clara de "não coberto" que permite ao motor deixar o glifo inalterado.
Substituição Única, os dois formatos
A Substituição Única corresponde ao LookupType 1 e mapeia um glifo para exatamente uma substituição. Possui também dois formatos, e a divisão constitui uma otimização de espaço. O Formato 1 guarda um único delta com sinal. O ID do glifo de saída é o ID do glifo de entrada mais esse delta, módulo 65536. É desta forma que uma fonte codifica uma substituição em que todos os glifos participantes se encontram a um desvio fixo idêntico da sua alternativa; por exemplo, um bloco de algarismos alinhados colocado a uma distância constante dos algarismos estilo antigo (oldstyle) correspondentes. A tabela de Cobertura indica quais os glifos que se qualificam, e o delta único serve a todos.
O Formato 2 guarda uma matriz explícita de IDs de glifos substitutos. O índice de cobertura obtido na tabela de Cobertura serve de índice para essa matriz, pelo que o glifo no índice de cobertura 0 passa a ser a primeira entrada da matriz, o índice de cobertura 1 a segunda, e assim sucessivamente. O Formato 2 é utilizado quando as alternativas não estão a um desvio uniforme, o que é o caso comum para conjuntos estilísticos criados manualmente. A consulta é idêntica do lado de quem efetua a chamada em ambos os casos. Recebe o glifo de entrada, passa-o pela Cobertura e, se estiver coberto, aplica o delta ou lê a posição correspondente na matriz.
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 comportamento digno de nota é a passagem direta. O método GetSingleSubstituteGlyph devolve o ID do glifo de entrada inalterado em qualquer situação de omissão: sem tipo de letra, sem tabela GSUB, sem funcionalidade correspondente ou sem correspondência de cobertura. Isto significa que a chamada pode ser feita de forma segura e incondicional. Pede a alternativa e, se não existir nenhuma, obtém de volta exatamente o que introduziu, pelo que o código que efetua a chamada nunca necessita de prever casos especiais para fontes que careçam da funcionalidade.
O significado das etiquetas de funcionalidades estilísticas
A etiqueta de funcionalidade constitui o vocabulário para definir que alternativa está a solicitar, sendo curta a lista de etiquetas relevantes para trabalhos estilísticos. O par principal é composto por salt (alternativas estilísticas), o acesso genérico a formas alternativas de um glifo, e de ss01 a ss20, os vinte conjuntos estilísticos numerados que uma fonte pode definir, consistindo cada um num agrupamento nomeado de substituições reunidas pelo designer. Uma fonte pode, por exemplo, colocar um a de nível único e um R de perna reta sob a etiqueta ss03, de modo que ativar esse conjunto específico altera o estilo de ambos.
Em torno destas, existem diversas outras etiquetas de substituição única. A etiqueta aalt representa o acesso a todas as alternativas (access-all-alternates), a união de todas as alternativas que um glifo possui, habitualmente apresentada como uma paleta de glifos. A etiqueta titl seleciona maiúsculas de título desenhadas para tamanhos grandes. As etiquetas subs e sups substituem algarismos por subscritos e sobrescritos reais, em vez de versões reduzidas dos algarismos predefinidos. A etiqueta ordn produz formas ordinais, as letras sobrelevadas em 1.º e 2.º. A etiqueta frac constrói frações, embora as frações diagonais completas também dependam de ligaduras e de lógica contextual que ultrapassa a substituição única simples. Para os casos de glifo único, o mecanismo é idêntico ao ss01: passa-se a etiqueta para a consulta de substituição e lê-se o glifo alternativo devolvido.
// 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;
O formato cmap 12 e os planos suplementares
Antes de qualquer substituição poder ser executada, um carácter tem de se tornar num glifo, e essa é a função da tabela cmap. A consulta de substituição começa a partir de um ID de glifo, por isso o percurso é sempre de carácter para glifo através da cmap e depois de glifo para alternativa através da GSUB. A parte interessante da tabela cmap é o seu alcance. Uma subtabela do formato 4 cobre o Plano Multilingue Básico (Basic Multilingual Plane - BMP), correspondente aos primeiros 65.536 pontos de código, o que é suficiente para a maioria dos textos latinos. Contudo, não é suficiente para pontos de código de U+10000 para cima, os planos suplementares, onde residem atualmente caracteres alfanuméricos matemáticos, muitos símbolos e diversos sistemas de escrita ativos.
O Formato 12 é a subtabela que cobre a totalidade do intervalo de U+0000 a U+10FFFF. Trata-se de uma lista ordenada de grupos, consistindo cada grupo num ponto de código inicial, num ponto de código final e num ID de glifo inicial, de modo que uma sequência contínua de pontos de código se mapeia numa sequência contínua de glifos. HotPDF resolve pontos de código recorrendo a uma estratégia híbrida que acompanha a estrutura dos dados. Os pontos de código no BMP são servidos a partir de uma matriz direta indexada pelo ponto de código, representando uma consulta única sem pesquisa. Os pontos de código nos planos suplementares são servidos a partir de uma tabela esparsa ordenada por ponto de código e pesquisada através de uma pesquisa binária. O resultado é que o GetUnicodeGlyphForCodepoint aceita um Cardinal completo e responde corretamente em todo o intervalo, devolvendo 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 terminam estas consultas
As APIs de substituição única respondem a um tipo específico de questão, importando esclarecer o que elas não resolvem. O LookupType 1 é um de oito tipos de substituição. A consulta não gere a substituição múltipla (LookupType 2), na qual um glifo se converte em vários, nem a substituição de ligadura (LookupType 4), na qual vários glifos se unem num só. Também não processa os tipos contextuais e contextuais em cadeia (LookupTypes 5 e 6), que só são acionados quando um glifo surge numa determinada vizinhança, nem os tipos de extensão e de cadeia invertida. Uma fração diagonal, uma conjunção em Devanagari ou uma cascata inicial-média-final em árabe são problemas de sequência, que uma pesquisa de substituição única por glifo não consegue expressar.
Também não realiza modelação (shaping) automática. Nada neste processo inspeciona um bloco de texto, decide que funcionalidades ativar e as aplica na ordem exigida pelo sistema de escrita. O autor da chamada escolhe a etiqueta da funcionalidade e aplica-a glifo a glifo. Esta é precisamente a ferramenta correta para conjuntos e alternativas estilísticas, que são opcionais e locais, e a ferramenta totalmente incorreta para um sistema de escrita que exija reordenação. Manter a fronteira bem definida é o que permite que o caminho de substituição permaneça simples e previsível.
Para os casos que exijam trabalho ao nível da sequência, o tratamento de escritas complexas é abordado no nosso artigo sobre modelação de texto de escrita complexa em Delphi. Se as suas substituições integram um trabalho de relatório mais amplo que também posiciona imagens e outros tipos de letra na página, o guia de saída de relatórios com tipos de letra e imagens descreve como essas peças se conjugam. Todas elas são executadas no mesmo motor, o HotPDF Component para Delphi e C++Builder, que integra as consultas de substituição GSUB a par das APIs de incorporação, subconjunto e texto de tipos de letra tratadas noutros locais deste blogue.