Artigo Técnico

Fontes variáveis em PDF: instanciação estática no Delphi

O PDF não tem nenhum conceito de fonte variável. Uma fonte incorporada em um arquivo PDF é um conjunto fixo de contornos com métricas fixas, então uma fonte variável precisa ser reduzida a uma única instância estática antes de poder entrar em um documento. O HotPDF realiza essa instanciação internamente: você inspeciona os eixos de uma fonte variável, seleciona coordenadas como peso 620 ou largura 87,5, e a biblioteca grava esses valores em um programa de fonte completo e autocontido que qualquer leitor de PDF em conformidade consegue renderizar

O motivo pelo qual isso importa é prático, não teórico. As fundições tipográficas cada vez mais distribuem um único arquivo variável em vez de uma dúzia de pesos estáticos, e as equipes de design escolhem valores que nenhuma instância nomeada oferece. Sem a instanciação, um gerador de relatórios ou recorre à instância padrão, o que descarta a decisão de design, ou incorpora a fonte variável inteira e espera que o visualizador respeite coordenadas de eixo que não tem como conhecer, o que nenhum leitor é obrigado a fazer

O que a instanciação realmente precisa reconstruir?

Uma fonte variável OpenType armazena um contorno padrão por glifo, além de um conjunto de deltas indexados por posição no espaço de design. Aplicar uma coordenada de eixo não é uma questão de escrever um número em um cabeçalho; significa percorrer a tabela gvar, interpolar os deltas para a posição solicitada, mover pontos e, em seguida, recalcular tudo o que foi derivado desses pontos. O HotPDF reconstrói os contornos dos glifos, a tabela longa loca, as métricas horizontais e verticais completas, a caixa delimitadora global da fonte e o ajuste de checksum do sfnt

Igualmente importante é o que é removido. Uma instância estática não deve manter fvar, avar, gvar, HVAR, VVAR, MVAR, STAT nem cvar, e um DSIG desatualizado também precisa ser removido, já que os bytes assinados deixaram de existir. Deixar qualquer um desses para trás produz uma fonte que afirma ser variável, mas carrega contornos que já foram movidos, e leitores que de fato aplicam variações acabam aplicando-as uma segunda vez

Pontos fantasmas e a armadilha da dupla aplicação

A regra mais sutil de todo o processo diz respeito às métricas. Em gvar, a contagem de pontos de um glifo abrange os pontos do contorno, ou os pontos de componente no caso de um glifo composto, mais quatro pontos fantasmas que codificam o recuo lateral esquerdo, a largura de avanço e seus equivalentes verticais. Esses pontos fantasmas também estão sujeitos a deltas

Assim, quando uma fonte tem uma tabela gvar, o HotPDF deriva as métricas horizontais e verticais a partir dos pontos fantasmas interpolados e não aplica adicionalmente HVAR ou VVAR. Aplicar os dois é o erro clássico: a mesma variação acaba sendo aplicada duas vezes, e cada largura de avanço sai um pouco larga demais, o que aparece como um texto que vai gradualmente deslizando para a direita ao longo de uma linha justificada. Só quando uma fonte não tem gvar a biblioteca grava a variação de métricas diretamente em hmtx ou vmtx

Dois outros detalhes mantêm a geometria correta. Pontos fantasmas nunca participam da interpolação de contorno, então pontos não listados explicitamente para um glifo simples são inferidos com IUP por contorno, excluindo os pontos fantasmas. E glifos compostos têm seus deltas aplicados aos deslocamentos de componente que usam parâmetros XY, após o que os limites dos filhos são recalculados recursivamente. Essa recursão tem profundidade limitada e é verificada contra ciclos, porque um grafo de componentes malicioso ou simplesmente corrompido poderia, do contrário, recursar sem fim

Inspecionando o espaço de design antes de escolher

A primeira chamada em qualquer fluxo de instanciação é InspectVariableFont, que informa os eixos e as instâncias nomeadas que a fundição definiu. Os registros de eixo carregam a tag de quatro bytes, os valores mínimo, padrão e máximo, flags e um ID de nome; as instâncias nomeadas carregam um ID de nome de subfamília, flags, um ID de nome PostScript opcional e uma coordenada por eixo:

var
  Pdf: THotPDF;
  Axes: THPDFVariableFontAxisArray;
  Instances: THPDFVariableFontNamedInstanceArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
    begin
      for I := 0 to High(Axes) do
        Writeln(Format('%s  min=%.1f default=%.1f max=%.1f',
          [string(Axes[I].Tag), Axes[I].MinimumValue,
           Axes[I].DefaultValue, Axes[I].MaximumValue]));
      Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
    end
    else
      Writeln('not a variable font - embed it as an ordinary TrueType face');
  finally
    Pdf.Free;
  end;
end;

Relatar o intervalo do eixo é importante porque os valores de eixo são limitados ao intervalo que a fonte declara, não ao intervalo que sua interface oferece. Um controle deslizante que permite ao usuário solicitar peso 1000 em uma fonte cujo eixo wght vai até 900 deve ser corrigido na interface, não silenciosamente na camada da fonte, ou a saída impressa vai divergir da pré-visualização

Selecionando coordenadas e emitindo o documento

A seleção de eixo tem estado e se aplica às fontes registradas depois dela. SetVariableFontAxis recebe uma tag ASCII imprimível de quatro bytes e um valor finito, e rejeita qualquer outra coisa com uma exceção em vez de ignorá-la silenciosamente. ClearVariableFontAxes reinicia a seleção, e GetVariableFontAxisSelections informa o que está pendente no momento, algo que vale a pena registrar em log em motores de relatório onde vários caminhos de código podem ter tocado o mesmo objeto de documento. A própria família é selecionada por nome por meio de SetFont, exatamente como qualquer outra face TrueType incorporada:

begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;

    Pdf.SetVariableFontAxis('wght', 620);   // semibold, não é uma instância nomeada
    Pdf.SetVariableFontAxis('wdth', 87.5);  // levemente condensada
    Pdf.CurrentPage.SetFont('Inter', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');

    Pdf.ClearVariableFontAxes;              // volta para a instância padrão
    Pdf.CurrentPage.SetFont('Inter', [], 10);
    Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

O evento OnVariableFontInstance dispara conforme cada instância é produzida e informa os valores de eixo que foram usados, a forma mais barata de comprovar em um log o que um determinado PDF realmente contém. Como cada conjunto distinto de coordenadas gera um programa de fonte distinto, trate as seleções de eixo como parte da sua chave de cache de fontes; a mecânica de cache está descrita em o cache persistente de subconjuntos de fontes

Como a instanciação interage com o subconjunto e a formatação (shaping)

A instanciação é executada antes do subconjunto (subsetting), e essa ordem é a correta. A fonte instanciada é uma face TrueType estática comum, então o subsetter comum a trata como qualquer outra: ele calcula o fechamento de glifos, mantém os glifos que o documento realmente usa e descarta o restante. A interação a se ter em mente é que duas seleções de eixo diferentes da mesma família são dois programas de fonte diferentes, então um documento que mistura peso 400 e peso 620 incorpora dois subconjuntos, não uma única face compartilhada com duas instâncias

A formatação (shaping) não é afetada em princípio, mas vale a pena verificar na prática. Os recursos de layout residem em GSUB e GPOS, que a instanciação preserva, então ligaduras e alternativas estilísticas continuam funcionando como descrito em alternativas estilísticas GSUB do OpenType. O que muda é o posicionamento: uma instância condensada tem avanços mais estreitos do que a padrão, então qualquer layout que tenha medido o texto antes da instanciação mediu larguras erradas. Meça com a mesma seleção de eixo com a qual você vai renderizar, e a discrepância desaparece

Uma última observação defensiva vinda da implementação, útil para quem for estender esse caminho. Fontes sem métricas verticais ainda avaliam argumentos de array dinâmico no ponto de chamada do Delphi, então os arrays de tempo de análise são sempre alocados, em vez de depender de uma verificação HasVerticalMetrics para interromper antecipadamente um índice vazio. É o tipo de detalhe de nível de linguagem que transforma um branch aparentemente protegido em uma violação de acesso exatamente nas fontes que você não testou

O suporte a fontes variáveis se encaixa no mesmo pipeline de fontes de incorporação, subconjunto e fechamento de glifos, descrito em mais profundidade em fechamento de subconjunto de fontes e glifos formatados. O conjunto completo de recursos de tipografia para Delphi e C++Builder está listado na página do componente PDF para Delphi HotPDF