Artigo Técnico

Fontes web em PDF: WOFF2 e eixos variáveis no Delphi

A PDF Library for Delphi aceita as fontes que um projeto web já usa. Ela importa contêineres WOFF2, enumera os eixos de design de uma fonte variável como JSON, e instancia um conjunto escolhido de coordenadas de eixo em uma fonte estática que os caminhos comuns de incorporação, subsetagem e extração tratam sem saber nada sobre variações. Tanto os contornos TrueType quanto os contornos CFF2 são cobertos

A situação que isso resolve é comum e irritante. A tipografia de uma marca é definida em um design system, o design system distribui arquivos WOFF2 variáveis, e o gerador de PDF não consegue ler nem o contêiner nem os dados de variação. A solução alternativa habitual — pedir à fundição instâncias TTF estáticas de todo peso realmente usado — leva semanas e produz arquivos que ninguém mantém sincronizados

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

Tratar WOFF2 como um wrapper Brotli e descomprimi-lo dá a você 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 em uma forma reorganizada que precisa ser revertida antes que a fonte seja analisável. Um descompressor de uso geral não consegue fazer isso

No Windows, a plataforma fornece análise e desempacotamento de contêiner desde o Windows 10 versão 1703, então a biblioteca usa a implementação do sistema em vez de distribuir outro decodificador. Essa decisão mantém a implantação livre de uma DLL extra e significa que a reversão de transformação é tratada pelo mesmo código que a própria pilha de texto do sistema operacional usa. Em outras plataformas, o WOFF1 ainda percorre um caminho Pascal puro e o WOFF2 explicitamente relata como não suportado, o que é uma resposta honesta em vez de uma fonte silenciosamente corrompida

O caminho de importação valida antes de decodificar e depois: o cabeçalho de 48 bytes, o comprimento do contêiner, os campos reservados, o tamanho sfnt declarado e um teto de 512 MiB, tudo é verificado primeiro, e o tamanho do fluxo decodificado é comparado com a declaração depois. Uma fonte web é conteúdo baixado, e os dois modos de falha que vale a pena bloquear são uma incompatibilidade de tamanho e uma bomba de descompressão

Este diagrama de pipeline mostra como a biblioteca PDF para Delphi importa uma fonte web WOFF2 por meio de validação de cabeçalho, decodificação do sistema, reversão de transformação sfnt e uma auditoria de tamanho pós-decodificação
Contêineres WOFF2 chegam como conteúdo baixado, então a importação valida o cabeçalho e os tamanhos declarados antes e depois da decodificaçã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 essa fonte realmente consegue fazer?
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // tags, mínimo, padrão, máximo por eixo

    // Fixa uma localização de design em uma 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 precisa reconstruir, e o que ela remove

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

Para contornos TrueType isso significa analisar fvar, avar, gvar, HVAR e MVAR, aplicar deltas de tupla com numeração de pontos compartilhada e privada, e interpolar os pontos que não carregam nenhum delta explícito. Pontos fantasmas horizontais, junto com HVAR, determinam a largura de avanço e o side bearing esquerdo. Glifos compostos precisam ter suas dependências de componente e mapeamentos de ponto resolvidos primeiro, depois translação, escala, matriz e deslocamentos de anexação aplicados — com limites de profundidade e detecção de ciclos, porque um grafo de componentes malicioso do contrário recursaria sem fim

Os metadados também se movem, e essa é a parte que a maioria das implementações pula. MVAR atualiza as métricas globais em hhea, OS/2 e post, de modo que o espaçamento de linha, a posição de sublinhado e o ângulo itálico correspondam à instância, e não ao padrão. Os eixos wght, wdth, ital e slnt adicionalmente atualizam a classe de peso, a classe de largura, as flags de seleção, o macStyle e o ângulo itálico, de modo que uma ferramenta que inspeciona a fonte estática relata o estilo que você pediu. Uma fonte cuja geometria é negrito e cujos metadados dizem regular será rotulada errado em toda parte adiante

A saída então achata contornos compostos e remove as tabelas que não se aplicam mais: as tabelas de variação, hints, tabelas de dispositivo e qualquer assinatura. Os limites de glifo, as métricas resumidas de hhea, os tetos 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 fonte variável reconstruindo uma face TrueType ao interpolar deltas de gvar, expandir glifos compostos, recalcular avanços e reescrever metadados de estilo antes de remover as tabelas de variação
A instanciação reconstrói tudo o que os deltas tocaram, depois remove o maquinário de variação, de modo que a incorporação comum enxerga uma fonte estática simples

O CFF2 percorre um caminho diferente até o mesmo lugar

O CFF2 não pode ser incorporado diretamente como um programa de fonte PDF: seus charstrings não carregam larguras, e ele depende das tabelas OpenType ao redor para ordem de glifo e métricas. Então instanciar uma fonte CFF2 produz, em vez disso, uma fonte OpenType CFF indexada por CID, que os caminhos existentes de desenho, incorporação e subsetagem já tratam

Carregar uma fonte CFF2 produz imediatamente uma instância estática de coordenada padrão para o documento atual, enquanto o sfnt CFF2 original é retido no objeto de fonte, de modo que a enumeração de eixos e a instanciação posterior ainda enxergam o espaço de design completo. Cada glifo tem suas sub-rotinas removidas individualmente durante a instanciação, o que elimina inteiramente a aritmética de variação e permite que o subsetter existente funcione sem mudanças. O custo é que uma instância estática pode ser ligeiramente maior do que uma que manteve sub-rotinas compartilhadas — um preço justo por não precisar de uma segunda implementação de subsetagem

Verificando uma instância antes de confiar nela

A instanciação é o tipo de recurso que pode estar sutilmente errado de formas que nenhuma exceção relata. Três verificações capturam quase tudo

Exporte a instância e abra-a em uma ferramenta de fonte. SaveFontToFile grava o programa de fonte retido, de modo que uma instância em memória pode ser despejada sem antes produzir um PDF. O arquivo exportado deve ser analisado sem erros, não conter nenhuma tabela de variação, e relatar os metadados de estilo que você solicitou

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

E extraia o texto de volta do PDF gerado. Uma instância que incorpora, mas produz texto não extraível, perdeu seu mapeamento ToUnicode, o que importa para busca e acessibilidade muito depois de a verificação visual ter passado. O caminho de extração mais amplo é abordado em extração de texto, imagem e fonte

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

// Pedir coordenadas fora do intervalo declarado as limita (clamp),
// então leia o JSON de eixos em vez de supor os limites de uma fonte
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

Notas práticas para um pipeline de fonte de marca

Armazene instâncias em cache por conjunto de coordenadas. Cada conjunto distinto de coordenadas é um programa de fonte distinto, então um documento misturando peso 400 e peso 620 incorpora dois subsets em vez de uma face compartilhada. Gerar a mesma instância repetidamente é trabalho desperdiçado; gerar dezenas de pesos quase idênticos é tamanho de arquivo desperdiçado

Meça com a instância com a qual você vai desenhar. Uma instância condensada tem avanços mais estreitos do que o padrão, então qualquer layout medido antes da instanciação mediu as larguras erradas. Essa é a causa isolada mais comum de texto que transborda de sua caixa apenas em produção

Mantenha um caminho alternativo para os glifos que a fonte da sua marca não tem. Uma fonte de marca variável tipicamente cobre latim e pouco mais, e um relatório contendo um nome de cliente em cirílico do contrário mostrará glifos ausentes; o mecanismo automático de fallback é descrito em fallback automático de fonte para glifos ausentes. E quando o objetivo é um arquivo menor em vez de um peso específico, a subsetagem faz mais trabalho do que a instanciação, como abordado em otimização de tamanho de arquivo PDF e subsetagem de fontes

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