Um designer escolhe um tipo de letra com um a de um só piso para os cabeçalhos, ou um zero cortado por uma barra para tabelas, ou um conjunto de maiúsculas ornamentadas (swash) para uma capa. Esses glifos já se encontram no tipo de letra. Simplesmente não são a predefinição. O a predefinido mapeia a partir do carácter através da tabela cmap para um glifo, e a alternativa encontra-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 significa ler a regra e emitir o glifo substituto no fluxo de conteúdo. Este artigo debruça-se sobre a leitura dessas regras, do tipo de substituição única, em Object Pascal sem qualquer biblioteca de formatação (shaping) nativa subjacente
O âmbito é restrito de forma propositada. Os conjuntos e alternativas estilísticas são substituições de um único glifo de entrada para um único glifo de saída. Constituem a parte do layout OpenType que é possível resolver com uma pequena e determinística passagem por tabelas, o que os torna bastante adequados para um motor Pascal que pretenda manter-se livre de dependências em C
Por que razão usar Delphi puro em vez do HarfBuzz
O HarfBuzz é a resposta óbvia para "formatar este texto", e para a formatação bidirecional completa, índica ou árabe, é a resposta correta. Trata-se, também, de uma biblioteca C. Integrá-la num produto Delphi ou C++Builder significa distribuir um objeto nativo para cada plataforma e arquitetura de destino, corresponder à sua convenção de chamadas, acompanhar a sua cadência de lançamentos e ler os termos da sua licença em comparação com os seus. Nada disto é difícil de forma isolada. Mas tudo isto constitui uma fricção que nunca desaparece e que não traz qualquer benefício quando o requisito real é "dê-me a forma ss01 desta letra"
A substituição única não requer um motor de formatação. Necessita de um analisador para uma mão cheia 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 pesquisas de substituição de glifos e nada mais. Não é uma resolução bidirecional (bidi), não é uma reordenação índica e não é uma formatação contextual automática. Quando estas são necessárias, são efetivamente necessárias e uma consulta de substituição única não irá substituí-las
A hierarquia GSUB, de cima a baixo
A tabela de Substituição de Glifos 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 de script (script tag) como latn seleciona uma entrada, e a etiqueta especial DFLT é o script predefinido que se aplica quando nenhum script mais específico corresponde. A entrada de script aponta para um LangSys, o sistema de idiomas, existindo um LangSys predefinido para o caso comum e opções com nome para idiomas que necessitam de um comportamento diferente. O turco é o exemplo habitual, onde o i com e sem ponto exigem o seu próprio tratamento
O LangSys nomeia 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, ss01 entre elas, e uma lista de índices de pesquisa (lookup indices). Esses índices apontam, por fim, para a LookupList, onde residem as verdadeiras subtabelas de substituição. Assim, resolver ss01 significa: encontrar o script, encontrar o seu LangSys, encontrar a funcionalidade cuja etiqueta é ss01, reunir as pesquisas por ela nomeadas e aplicá-las. O HotPDF utiliza, por predefinição, o script DFLT e o LangSys predefinido, que é o que a vasta maioria dos designs de texto latino envia, e expõe uma forma de substituir a etiqueta do script quando um tipo de letra associa as suas funcionalidades a um script específico
As tabelas Coverage decidem quem participa
Cada subtabela de substituição inicia-se com a mesma questão: será que este glifo de entrada participa nesta regra e, em caso afirmativo, onde se situa na indexação da própria regra? Esta questão é respondida por uma tabela Coverage, e a resposta é um índice de cobertura, um pequeno ordinal que o resto da subtabela utiliza para procurar no que o glifo se converte
A Coverage é apresentada em dois formatos. O Formato 1 é uma lista de IDs de glifos ordenada de forma ascendente. Encontra-se 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, sendo cada um deles constituído por um glifo inicial, um glifo final e o índice de cobertura para o qual o glifo inicial é mapeado. Um glifo inserido num intervalo obtém o seu índice de cobertura calculando o desvio a partir do início do intervalo. O Formato 1 é compacto quando os glifos participantes se encontram dispersos, e o Formato 2 é vantajoso quando surgem em sequências contíguas. Ambos estão ordenados, pelo que ambos são pesquisados em tempo logarítmico e ambos retornam um índice de cobertura ou um claro "não coberto" que permite ao motor deixar o glifo em paz
Substituição Única, os dois formatos
A Substituição Única corresponde a 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 armazena 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 um tipo de letra codifica uma substituição na qual cada glifo participante se encontra no mesmo desvio fixo em relação à sua alternativa; por exemplo, um bloco de algarismos alinhados colocado a uma distância constante dos algarismos de estilo antigo correspondentes. A tabela Coverage indica quais os glifos qualificados e o delta serve para todos eles
O Formato 2 armazena um array explícito de IDs de glifos substitutos. O índice de cobertura da tabela Coverage é o índice nesse array, pelo que o glifo no índice de cobertura 0 torna-se a primeira entrada do array, o índice de cobertura 1 a segunda, e assim sucessivamente. O Formato 2 é utilizado quando as alternativas não se encontram num desvio uniforme, o que é um caso comum para conjuntos estilísticos criados de forma manual. A consulta é a mesma do lado de quem efetua a chamada, de qualquer forma. Pega no glifo de entrada, executa-o através de Coverage e, caso este seja coberto, aplica o delta ou lê 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 digno de nota é a passagem (pass-through). O GetSingleSubstituteGlyph retorna o ID do glifo de entrada inalterado em todas as falhas: sem tipo de letra, sem tabela GSUB, sem correspondência de funcionalidade, sem acerto de cobertura. Isto significa que a chamada é segura para ser efetuada de modo incondicional. Pede-se a alternativa e, caso esta não exista, o retorno é exatamente igual ao que foi introduzido, pelo que o código responsável pela chamada nunca tem de tratar especificamente um tipo de letra que não possua a funcionalidade
O que significam as etiquetas de funcionalidade estilística
A etiqueta da funcionalidade representa todo o vocabulário acerca da alternativa pretendida, e as etiquetas relevantes para trabalho estilístico constituem uma lista curta. O par de destaque é o salt, alternativas estilísticas (o acesso genérico às formas alternativas de um glifo), e de ss01 até ss20, os vinte conjuntos estilísticos numerados que um tipo de letra pode definir, cada um sendo um conjunto nomeado de substituições que o designer agrupa. Um tipo de letra pode colocar um a de um só piso e um R de perna reta em ss03, por exemplo, pelo que a ativação deste conjunto altera o estilo de ambos
Junto a estes, existem ainda várias etiquetas de substituição única. O aalt refere-se a 'access-all-alternates' (aceder a todas as alternativas), a união de cada alternativa que um glifo tem, normalmente apresentada como uma funcionalidade da paleta de glifos. O titl seleciona maiúsculas de titulação cortadas para tamanhos grandes. O subs e sups trocam por algarismos que são verdadeiros subscritos e sobrescritos, em vez das predefinições reduzidas. O ordn produz formas ordinais, as letras levantadas em 1º e 2º. O frac constrói frações, embora as frações diagonais completas também se baseiem na lógica contextual e de ligatura, a qual ultrapassa a simples substituição única. Para os casos de glifo único, o mecanismo é idêntico a 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;
cmap de formato 12 e os planos suplementares
Antes de qualquer substituição se processar, um carácter tem de se tornar um glifo, e essa é a tarefa da tabela cmap. A consulta de substituição inicia-se a partir do ID de um glifo, de forma que o caminho é sempre carácter a glifo por via do cmap e, depois, de glifo a alternativa por via do GSUB. A parte interessante do cmap é o seu alcance. Uma subtabela de formato 4 abrange o Plano Multilingue Básico (BMP), os primeiros 65536 pontos de código, e isso basta para a grande parte do texto latino. No entanto, tal não é suficiente para pontos de código de U+10000 em diante, os planos suplementares, local onde residem agora alfanuméricos matemáticos, vários símbolos e alguns scripts atualmente em uso
O formato 12 é a subtabela que cobre a gama total de U+0000 até U+10FFFF. É uma lista ordenada de grupos, sendo cada grupo composto por um ponto de código inicial, um ponto de código final e o ID de glifo inicial, assim uma série contígua de pontos de código é mapeada numa série contígua de glifos. O HotPDF resolve pontos de código recorrendo a uma estratégia híbrida que coincide com a forma como os dados estão organizados. Os pontos de código no BMP são apresentados por meio de um array direto indexado pelo ponto de código, uma única consulta sem qualquer pesquisa. Os pontos de código nos planos suplementares são apresentados mediante uma tabela esparsa, ordenada por ponto de código e percorrida através de uma pesquisa binária. A consequência disto é que o GetUnicodeGlyphForCodepoint recebe um Cardinal completo e responde corretamente em todo o intervalo, retornando o ID do glifo 0, o glifo .notdef, para qualquer ponto de código que o tipo de letra 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 param estas consultas
As APIs de substituição única respondem a uma única forma de questão e convém sermos claros em relação ao que não respondem. O LookupType 1 é um dos oito tipos de substituição. A consulta não gere substituição múltipla LookupType 2, onde um glifo se torna vários, nem substituição de ligatura LookupType 4, onde múltiplos glifos se reúnem num só. Não gere os tipos contextuais e os contextuais em cadeia, LookupTypes 5 e 6, que são acionados exclusivamente quando um glifo é apresentado numa vizinhança particular, nem sequer os tipos de extensão ou encadeamento reverso. Uma fração na diagonal, uma união do tipo devanágari ou uma cascata árabe do tipo inicial-medial-final consubstanciam um problema de sequência que a procura de substituição única (realizada glifo a glifo) não tem a capacidade de expressar
Do mesmo modo, a formatação automática também não é efetuada. Nada do que vimos permite inspecionar uma área de texto, decidir as funcionalidades a ativar e aplicá-las na ordem exigida pelo script. Quem efetua a chamada escolhe a etiqueta de funcionalidade pretendida e aplica-a a cada glifo. Esta é, precisamente, a ferramenta ideal para os conjuntos estilísticos e alternativas que exigem inclusão (opt-in) e são localizadas, sendo a pior opção de ferramenta para um script que implique reordenação. É a manutenção rigorosa desta barreira que permite que o processo de substituição se mantenha curto e previsível
Para os casos que requeiram realmente um trabalho a nível da sequência, a questão dos scripts complexos é abordada no nosso artigo sobre formatação de textos de scripts complexos em Delphi. Caso as suas substituições façam parte de um trabalho de elaboração de relatório mais amplo que também introduza na página as imagens e os restantes tipos de letra, o guia de exportação de relatórios com os tipos de letra e imagens descreve de que forma estes elementos se conjugam. Todos correm a partir de um mesmo motor: o Componente HotPDF concebido para o Delphi e para o C++Builder. Este comporta as consultas referentes a substituições da tabela GSUB em simultâneo com a incorporação de tipos de letra, o subconjunto de carateres (subsetting) bem como as APIs baseadas em texto referidas em vários pontos da nossa plataforma (blogue)