Artigo Técnico

Fontes Web em PDF: WOFF2 e Eixos Variáveis em Delphi

A PDF Library for Delphi aceita as fontes que um projeto web já distribui. Importa contentores WOFF2, enumera os eixos de design de uma fonte variável como JSON, e instancia um conjunto escolhido de coordenadas de eixo numa fonte estática que os percursos comuns de incorporação, subconjuntação e extração tratam sem saber nada sobre variações. São cobertos tanto os contornos TrueType como os contornos CFF2

A situação que isto resolve é comum e irritante. A tipografia de uma marca está definida num design system, o design system distribui ficheiros WOFF2 variáveis, e o gerador de PDF não consegue ler nem o contentor nem os dados de variação. A solução alternativa habitual — pedir à fundição tipográfica instâncias TTF estáticas de todos os pesos realmente utilizados — demora semanas e produz ficheiros que ninguém mantém sincronizados

Porque é que o WOFF2 não é apenas uma fonte comprimida?

Tratar o WOFF2 como um invólucro Brotli e descomprimi-lo dá bytes que não são um sfnt válido. O formato também permite tabelas transformadas e reordenadas: glyf, loca e hmtx podem ser armazenadas numa forma reorganizada que tem de ser revertida antes de a fonte ser analisável. Um descompressor de uso geral não consegue fazer isso

No Windows, a plataforma fornece análise e desempacotamento de contentores desde o Windows 10 versão 1703, pelo que a biblioteca usa a implementação do sistema em vez de distribuir outro descodificador. Essa decisão mantém a implementação livre de uma DLL extra e significa que a reversão da transformação é tratada pelo mesmo código que a própria pilha de texto do sistema operativo utiliza. Noutras plataformas, o WOFF1 ainda percorre um percurso em Pascal puro, e o WOFF2 é explicitamente reportado como não suportado, o que é uma resposta honesta, em vez de uma fonte silenciosamente corrompida

O percurso de importação valida antes de descodificar e depois: o cabeçalho de 48 bytes, o comprimento do contentor, os campos reservados, o tamanho sfnt declarado e um limite de 512 MiB são todos verificados primeiro, e o tamanho do stream descodificado é depois comparado com a declaração. Uma fonte web é conteúdo descarregado, e os dois modos de falha que vale a pena bloquear são uma discrepância de tamanho e uma bomba de descompressão

Este diagrama de pipeline mostra como a biblioteca PDF para Delphi importa uma fonte web WOFF2 através de validação de cabeçalho, descodificação pelo sistema, reversão da transformação sfnt e uma auditoria de tamanho pós-descodificação
Os contentores WOFF2 chegam como conteúdo descarregado, pelo que a importação valida o cabeçalho e os tamanhos declarados antes e depois da descodificação
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  FontID, InstanceID: Integer;
  AxesJSON: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    // WOFF2, WOFF1, TTF, OTF e TTC entram todos pela mesma chamada
    FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
    if FontID <= 0 then
      raise Exception.Create('font could not be imported');

    // O que é que esta fonte realmente consegue fazer?
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // tags, mínimo, predefinição, máximo por eixo

    // Gravar uma localização de design numa fonte estática
    InstanceID := Lib.CreateOpenTypeFontVariation(FontID,
      'wght=620,wdth=87.5');

    Lib.DrawOpenTypeText(56, 760, InstanceID, 24, 'Quarterly results', 0);
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

O que a instanciação tem de reconstruir, e o que remove

Uma fonte variável armazena um contorno predefinido por glifo, mais deltas indexados por posição no espaço de design. Produzir uma instância estática significa interpolar esses deltas nas coordenadas pedidas, mover os pontos, e depois recalcular tudo o que deles deriva

Para contornos TrueType isso significa analisar fvar, avar, gvar, HVAR e MVAR, aplicar deltas de tuplos com numeração de pontos partilhada e privada, e interpolar os pontos que não transportam delta explícito. Os pontos fantasma horizontais, em conjunto com HVAR, determinam a largura de avanço e o rebordo esquerdo. Os glifos compostos precisam de ter primeiro as suas dependências de componentes e mapeamentos de pontos resolvidos, e só depois aplicados os deslocamentos de translação, escala, matriz e fixação — com limites de profundidade e deteção de ciclos, porque um grafo de componentes malicioso recursaria de outro modo sem fim

Os metadados também se movem, e esta é a parte que a maioria das implementações ignora. MVAR atualiza as métricas globais em hhea, OS/2 e post, pelo que o espaçamento entre linhas, a posição do sublinhado e o ângulo itálico correspondem à instância, e não à predefinição. Os eixos wght, wdth, ital e slnt atualizam adicionalmente a classe de peso, a classe de largura, as flags de seleção, macStyle e o ângulo itálico, pelo que uma ferramenta que inspecione a fonte estática reporta o estilo pedido. Uma fonte cuja geometria é negrito e cujos metadados dizem regular ficará mal identificada em todo o lado a jusante

A saída depois achata os contornos compostos e remove as tabelas que já não se aplicam: as tabelas de variação, os hints, as tabelas de dispositivo e qualquer assinatura. Os limites dos glifos, as métricas de resumo de hhea, os limites de hint de maxp, os checksums por tabela e o checksum da fonte inteira são todos recalculados

Este diagrama mostra a instanciação de fontes variáveis a reconstruir uma face TrueType, interpolando deltas de gvar, expandindo glifos compostos, recalculando avanços e reescrevendo os metadados de estilo antes de remover as tabelas de variação
A instanciação reconstrói tudo o que os deltas tocaram, e depois remove o mecanismo de variação, para que a incorporação comum veja uma fonte estática simples

O CFF2 percorre um caminho diferente até ao mesmo destino

O CFF2 não pode ser incorporado diretamente como um programa de fonte PDF: as suas charstrings não transportam larguras, e depende das tabelas OpenType circundantes para a ordem de glifos e as métricas. Assim, instanciar uma fonte CFF2 produz, em vez disso, uma fonte OpenType CFF indexada por CID, que os percursos existentes de desenho, incorporação e subconjuntação já tratam

Carregar uma fonte CFF2 produz imediatamente uma instância estática de coordenadas predefinidas para o documento atual, enquanto o sfnt CFF2 original é retido no objeto de fonte, para que a enumeração de eixos e a instanciação posterior continuem a ver o espaço de design completo. Cada glifo é de-subrotinizado individualmente durante a instanciação, o que remove por completo a aritmética de variação e permite que o subconjuntador existente funcione sem alterações. A contrapartida é que uma instância estática pode ser ligeiramente maior do que uma que mantivesse subrotinas partilhadas — um preço justo por não precisar de uma segunda implementação de subconjuntação

Verificar uma instância antes de confiar nela

A instanciação é o tipo de funcionalidade que pode estar subtilmente errada de formas que nenhuma exceção reporta. Três verificações apanham quase tudo

Exporte a instância e abra-a numa ferramenta de fontes. SaveFontToFile escreve o programa de fonte retido, pelo que uma instância em memória pode ser despejada sem primeiro produzir um PDF. O ficheiro exportado deve analisar-se sem erros, não conter tabelas de variação, e reportar os metadados de estilo pedidos

Compare os avanços numa localização não predefinida. Se o seu pipeline conseguir produzir a mesma instância de outra forma, compare as larguras de avanço e os limites das curvas glifo a glifo; a implementação de referência aqui igualou uma instanciação independente de uma fonte de dois eixos com 313 glifos, com uma diferença máxima de zero em todos os glifos

E volte a extrair o texto do PDF gerado. Uma instância que se incorpora mas não produz texto extraível perdeu o seu mapeamento ToUnicode, o que importa para a pesquisa e a acessibilidade muito depois de a verificação visual ter passado. O percurso de extração mais amplo está descrito em extração de texto, imagem e fonte

Este diagrama mostra três verificações para uma instância de fonte variável gerada: despejar o ficheiro de fonte exportado, comparar os avanços de glifo contra uma instanciação independente e extrair o texto de volta do PDF
Uma instância subtilmente errada não lança nenhuma exceção, pelo que o ficheiro exportado, os avanços de glifo e o texto extraído são cada um verificados, por sua vez
// Despejar uma instância gerada para inspeção ou reutilização
Lib.SaveFontToFile('Inter-620-87.ttf');

// Pedir coordenadas fora do intervalo declarado faz com que sejam fixadas (clamped),
// por isso leia o JSON dos eixos, em vez de presumir os limites de uma fonte
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

Notas práticas para um pipeline de fonte de marca

Guarde instâncias em cache por conjunto de coordenadas. Cada conjunto distinto de coordenadas é um programa de fonte distinto, pelo que um documento que mistura peso 400 e peso 620 incorpora dois subconjuntos, em vez de uma face partilhada. Gerar a mesma instância repetidamente é trabalho desperdiçado; gerar dezenas de pesos quase idênticos é tamanho de ficheiro desperdiçado

Meça com a instância com que vai desenhar. Uma instância condensada tem avanços mais estreitos do que a predefinição, pelo que qualquer layout que tenha medido antes da instanciação mediu as larguras erradas. Esta é, isoladamente, a causa mais comum de texto que transborda da sua caixa apenas em produção

Mantenha um percurso de reserva (fallback) para os glifos que faltam na sua fonte de marca. Uma fonte de marca variável cobre tipicamente o latino e pouco mais, e um relatório com um nome de cliente cirílico mostrará, de outro modo, glifos em falta; o mecanismo de fallback automático está descrito em fallback automático de fontes para glifos em falta. E quando o objetivo é um ficheiro mais pequeno, em vez de um peso específico, a subconjuntação faz mais trabalho do que a instanciação, como descrito em otimização do tamanho de ficheiro PDF e subconjuntação de fontes

A importação de fontes web, a instanciação variável, a subconjuntação e o fallback fazem parte do mesmo pipeline de fontes para Delphi, C++Builder e Free Pascal; a lista completa de funcionalidades está disponível na página da PDF Library for Delphi