O HotPDF embute fontes variáveis OpenType com outlines CFF2 convertendo-as, nos valores de eixo selecionados, num programa de fonte CFF1 com chave CID que os leitores de PDF já entendem. Texto nativo comum registrado via RegisterUnicodeTTF usa as tabelas cmap e hmtx da própria fonte, e o writer embute o programa convertido sob /FontFile3 com /Subtype /CIDFontType0C, mantendo todo CID igual ao glyph ID original
A rota óbvia vale a descrição, porque falha sem avisar ninguém. Parseie e armazene em cache as tabelas CFF2, depois deixe o caminho de texto simples resolver a fonte pelo Windows GDI com um match só de TrueType: o GDI não faz ideia do que fazer com outlines CFF2 e silenciosamente substitui por ArialUnicodeMS. Tudo a jusante que leia a fonte substituta recebe os outlines errados, as larguras erradas e a extração errada para caracteres do plano suplementar. O PDF abre bem. Ele simplesmente não está composto na fonte que você pediu, e é por isso que o caminho de texto precisa trabalhar a partir dos bytes registrados em vez de uma consulta GDI
Por que um PDF não pode embutir uma fonte CFF2 diretamente?
O PDF não tem slot para um programa CFF2. A ISO 32000-2 §9.9 lista os tipos de programa de fonte embutida que um leitor precisa tratar: Type 1 sob /FontFile, TrueType sob /FontFile2, e sob /FontFile3 um programa CFF puro (Type1C ou CIDFontType0C) ou um wrapper OpenType em torno de outlines TrueType ou CFF. Todo sabor CFF dessa lista significa CFF versão 1, o formato que os leitores parseiam há duas décadas
CFF2 é um formato diferente que por acaso compartilha o nome. Ele derruba o INDEX de Name e String, derruba o charset, tira os avanços de glifo dos CharStrings e os deixa para o hmtx, e adiciona operadores blend para um único outline descrever todo o design space. Um parser CFF1 que encontra uma tabela CFF2 não tem base para chutar, e nada na especificação do PDF o obriga a tentar. Envolver a fonte CFF2 num stream OpenType e torcer para o melhor é exatamente o tipo de arquivo que renderiza numa máquina e falha na outra, então o HotPDF não faz isso
A conversão também precisa ser estática. Como o artigo sobre instâncias estáticas de fontes variáveis explica, uma fonte PDF é um conjunto fixo de outlines com métricas fixas, e nenhum leitor é obrigado a aplicar coordenadas de eixo. O HotPDF então resolve a variação primeiro e só depois grava CFF1
Escrevendo texto nativo numa fonte CFF2
Para o código da aplicação, uma fonte CFF2 se comporta como qualquer outra fonte Unicode registrada: selecione os valores de eixo, registre o arquivo, selecione a família pelo nome e desenhe. O RegisterUnicodeTTF guarda uma cópia separada da tabela de nomes original, então o SetFont bate com o nome de família, nome completo, nome PostScript ou nome de família tipográfica e seleciona os bytes da fonte estática congelada, sem instalação no Windows nem consulta GDI
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'brochure.pdf';
Pdf.BeginDoc;
// Valores de eixo se aplicam às fontes registradas após esta chamada
Pdf.SetVariableFontAxis('wght', 650);
Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
// Nome de família, completo, PostScript ou família tipográfica da tabela de nomes
Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Os glyph IDs vêm da cmap registrada e os avanços da hmtx registrada. Um glifo cujo avanço é genuinamente zero continua zero em vez de recuar para uma medição GDI, e um par surrogate UTF-16 produz exatamente um avanço, não dois. Caracteres BMP, caracteres do plano suplementar e portadores sintéticos de shaping mantêm mapeamentos Unicode por fonte, então trocar para outra fonte embutida ou começar uma página nova não deixa a outra fonte atualizar o estado de subset desta fonte nem tomar os glyph IDs dela emprestados
A medição segue os mesmos bytes. THPDFPage.GetWideTextWidth e THPDFPage.GetWideCharAdvances leem as métricas da fonte selecionada lastreada em bytes, então quebra de linha e alinhamento à direita concordam com o que é desenhado. Um detalhe de ordem pega muita gente: mudanças no SetVariableFontAxis feitas depois do registro afetam a próxima fonte que você registrar, não a instância já congelada. Para obter um segundo peso, ajuste o novo valor e registre de novo
Como o HPDFConvertCFF2ToCFF constrói o programa com chave CID?
O HPDFConvertCFF2ToCFF produz um programa CFF1 bruto com chave CID de um único Font DICT, no qual o CID iguala o glyph ID original em cada slot, incluindo os vazios e não usados. Essa identidade é todo o design. O nome da fonte, as larguras /W do PDF, os avanços CFF e a CMap ToUnicode se referem todos à mesma numeração, então nenhuma camada precisa traduzir entre glyph IDs e CIDs, e nada pode dessincronizar
No lado PDF o descendente é uma fonte CIDFontType0 com encoding Identity-H ou Identity-V, e o stream do programa fica sob /FontFile3 com /Subtype /CIDFontType0C. Como o CID já iguala o GID, o descendente não carrega /CIDToGIDMap, e o stream fica fora do hook posterior de substituição de streams usado para fontes TrueType registradas. Diferente do wrapper /Subtype /OpenType, o stream convertido não eleva o requisito de versão PDF 1.6
As larguras precisaram de um pequeno truque. Um CharString Type 2 de CFF1 armazena o avanço como um primeiro operando opcional relativo ao nominalWidthX, e operandos inteiros ficam limitados à faixa com sinal de 16 bits. Um avanço OpenType é um valor sem sinal de 16 bits, até 65535. O HotPDF põe o nominalWidthX em 32768 e o defaultWidthX em 0, e então grava cada avanço explicitamente como advance - 32768. Um avanço zero vira -32768 e um avanço 65535 vira 32767, então toda a faixa uint16 é representável sem clampar um único glifo
A geometria recebe o mesmo cuidado. O FontMatrix é [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], então uma fonte de 2048 unidades mantém as coordenadas nativas. Movimentos e linhas relativos grandes demais para um operando são divididos em passos representáveis, e curvas cúbicas são subdivididas de um jeito que preserva a forma. Os registros de copyright e licença originais, a tabela de nomes original e as flags de embedding viajam junto na string Notice do CFF
Chamando o conversor fora de um documento
O conversor é uma função pública na unit HPDFOpenTypeAdvanced, escrita em Pascal, sem dependência de HarfBuzz e sem etapa de instalação de fonte no sistema. Isso o torna utilizável por conta própria, por exemplo para pré-converter uma fonte no build ou inspecionar o que o writer de PDF vai embutir
uses
System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;
procedure ConvertWeight650;
var
Source, Cff: TBytes;
Axes: THPDFVariableFontAxisValueArray;
begin
Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
SetLength(Axes, 1);
Axes[0].Tag := 'wght';
Axes[0].Value := 650;
Cff := HPDFConvertCFF2ToCFF(Source, Axes);
if Length(Cff) = 0 then
raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;
Entrada variável passa por instancing estático só de geometria nas coordenadas dadas, cobrindo outlines e avanços. Uma fonte CFF2 estática aceita só uma seleção vazia; passar valores de eixo a ela é tratado como erro em vez de ignorado. O resultado é um programa CFF1 puro, não um contêiner sfnt, então não o entregue a código que espera um arquivo OpenType
Falha é um TBytes vazio, não uma exceção, para os casos esperados: entrada que não é CFF2, uma seleção de eixo que o instancer não aceita, valores não finitos como NaN e fontes cujo OS/2 fsType define embedding de licença restrita (0x0002) ou apenas bitmap (0x0200). Sempre teste o comprimento. Dentro do EndDoc o writer faz isso por você e levanta "StoreFont: CFF2 outline conversion failed", que é a chamada certa: um programa de fonte em branco num PDF enviado ao cliente é bem pior que um erro visível no seu log
Orçamentos e cancelamento para workloads de servidor
O terceiro parâmetro opcional, THPDFVariableFontControl, limita memória e permite cancelar uma conversão longa. Sem ele o conversor usa os seus defaults: um limite de 128 MiB na entrada e na saída da fonte, um orçamento de trabalho acumulado de 1 GiB e o limite CFF1 de 65535 bytes por CharString
type
TFontJob = class
private
FCancelled: Boolean;
procedure CheckCancel;
public
function Convert(const Source: TBytes): TBytes;
property Cancelled: Boolean read FCancelled write FCancelled;
end;
procedure TFontJob.CheckCancel;
begin
if FCancelled then
Abort; // se propaga para fora do HPDFConvertCFF2ToCFF
end;
function TFontJob.Convert(const Source: TBytes): TBytes;
var
Control: THPDFVariableFontControl;
begin
Control := THPDFVariableFontControl.Create(
32 * 1024 * 1024, // máximo de bytes de saída
Int64(256) * 1024 * 1024, // orçamento de trabalho acumulado
CheckCancel);
try
// seleção nil: instância default de uma fonte variável, ou uma fonte CFF2 estática
Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
finally
Control.Free;
end;
end;
Estouro de orçamento e cancelamento são reportados como exceções, deliberadamente distintos do caminho de resultado vazio. Um array vazio significa que a fonte não pode ser convertida; uma exceção significa que esta tentativa foi interrompida. Mantenha os dois separados no seu tratamento de erros, porque retentar uma fonte não conversível com um orçamento maior não vai ajudar
Quanto pesa uma fonte CFF2 embutida?
O principal a planejar é o tamanho do arquivo: o HotPDF embute o conjunto completo de glifos convertido para fontes CFF2, com EnableFontSubsetting ligado ou desligado. Uma fonte com vários milhares de glifos adiciona todos eles a cada documento que a usa, mesmo que a página mostre um título. Para um folheto isso é aceitável. Para um servidor de relatórios escrevendo milhares de PDFs pequenos é um custo real que você deve medir antes de se comprometer
Os recursos de tamanho relacionados seguem a mesma regra. Fontes CFF2 convertidas são embutidas inteiras, então nem os subsets compactos de fonte ativados pelo CompressDocument nem o cache persistente de subset de fontes se aplicam a elas. Fontes TrueType no mesmo documento continuam com subset e cache como de costume. O benefício de manter todos os glifos é que fontes cuja licença proíbe subsetting são embutidas de uma forma que o respeita
Os limites de recursos são limites finitos de conversão, e o próprio CFF1 limita o que um único CharString pode conter. Um glifo cujo CharString convertido excede 65535 bytes, ou uma fonte que estoura o orçamento de trabalho, falha visivelmente em vez de ser gravada com um outline vazio. Esse é o trade-off pretendido: uma falha barulhenta numa fonte exótica vence um documento com glifos silenciosamente ausentes
Texto nativo CFF2 e o conversor CFF1 portátil vêm com o componente HotPDF Delphi PDF para Delphi e C++Builder, ao lado dos recursos de instancing de fontes variáveis, glifos coloridos e subsetting de fontes cobertos em outros artigos deste blog