Artigo Técnico

Subconjuntos de Fontes TrueType via fontsub.dll em Delphi

O HotXLS, o componente Excel para Delphi e C++Builder, reduz o tamanho de fontes PDF incorporadas através da criação de subconjuntos de fontes TrueType: no momento da exportação para PDF, chama a função CreateFontPackage da biblioteca de sistema do Windows fontsub.dll para reconstruir uma fonte TrueType incorporada à volta apenas dos pontos de código Unicode que uma folha de cálculo efetivamente utilizou, em vez de distribuir o ficheiro de tipo de letra completo. Um relatório com duzentas linhas de nomes de produtos em chinês pode precisar apenas de umas centenas de caracteres Han distintos, e no entanto as fontes CJK que o Windows distribui rondam rotineiramente os 5 a 20 MB cada. Incorporar uma inteira, e a fonte por si só pode pesar mais do que todos os outros objetos do PDF juntos

fontsub.dll não é uma biblioteca de que a maioria dos programadores Delphi alguma vez tenha ouvido falar, e há uma razão para isso: a Microsoft distribui-a como uma pequena DLL utilitária, escassamente documentada, em vez de uma API Win32 de destaque. O HotXLS trata-a como uma capacidade opcional, não uma dependência rígida, pelo que a forma como o exportador a carrega, a chama, e recua quando ela está ausente diz tanto sobre programação defensiva no Windows como sobre formatos de fontes, e ambas as metades dessa história vale a pena percorrer

Porque incha o texto Unicode uma exportação de PDF do HotXLS?

O exportador de PDF do HotXLS só recorre a uma fonte TrueType incorporada quando o texto da folha de cálculo sai fora do WinAnsi, e permanece na família Helvetica incorporada o resto do tempo, o caminho predefinido que o guia de exportação de folha de cálculo para PDF cobre em profundidade. O WinAnsi cobre texto da Europa Ocidental suficientemente bem para que muitos livros de cálculo nunca disparem sequer uma incorporação de fonte: o PDF limita-se a referenciar o Helvetica pelo nome e o leitor fornece-o localmente, pelo que o ficheiro se mantém pequeno. No momento em que uma célula contém algo que o WinAnsi não consegue representar, um nome de produto chinês, uma nota em coreano, um símbolo perdido num comentário, o exportador tem de incorporar um verdadeiro programa de fonte, porque um leitor de PDF não tem qualquer fonte de glifos de reserva para caracteres fora das 14 fontes padrão

O HotXLS localiza essa fonte automaticamente, analisando a pasta de Fontes do Windows em busca de uma pequena lista de candidatas instaladas, incluindo os tipos de letra com capacidade CJK que o Windows distribui para renderização em chinês e coreano, a menos que a propriedade UnicodeFontFile do exportador já aponte para um ficheiro específico, e seja qual for a fonte em que acabe, é incorporada inteira antes de a criação de subconjuntos sequer correr. Esse requisito de incorporação é específico do PDF: os caminhos de exportação RTF e HTML do HotXLS mantêm o texto Unicode intacto ao escapar pontos de código no fluxo de bytes, em vez de distribuir um programa de fonte, razão pela qual o problema de tamanho aqui abordado não tem equivalente nesses dois formatos

uses
  lxHandle, lxPDF;

var
  Book: TXLSWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('catalog-cn.xlsx');
    Exporter := TXLSPDFExport.Create;
    try
      // Optional: pin a specific CJK-capable font instead of the
      // exporter's automatic Windows\Fonts scan.
      Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
      Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

O que é a fontsub.dll, e porque não escrever um criador de subconjuntos de raiz?

fontsub.dll é uma pequena biblioteca de sistema do Windows, distribuída desde o Windows XP, que expõe uma única função relevante aqui: CreateFontPackage. Entregue-lhe os bytes de uma fonte TrueType de origem e uma lista de pontos de código Unicode a manter, e ela devolve uma fonte mínima que continua a satisfazer todas as restrições do formato de fonte: índices de glifo renumerados, glyf e loca reconstruídos apenas à volta dos contornos retidos, hmtx e cmap reescritos para corresponder. O HotXLS declara o tipo de ponteiro de função diretamente contra esse contrato

const
  TTFCFP_FLAGS_SUBSET = 1;
  TTFMFP_SUBSET = 0;
  TTFCFP_MS_PLATFORMID = 3;
  TTFCFP_UNICODE_CHAR_SET = 1;

type
  TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
    var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
    var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
    usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
    pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
    lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;

Escrever à mão o trabalho do CreateFontPackage, em vez de o chamar, significaria implementar um criador de subconjuntos TrueType correto: percorrer glifos compostos para trazer cada glifo componente que um glifo retido referencia, reconstruir os deslocamentos de loca depois de contornos serem descartados, respeitar os bits de permissão de incorporação na tabela OS/2 de uma fonte, e acertar em tudo isso através de fontes excêntricas quaisquer que a máquina de um cliente calhe de ter instaladas. A Microsoft já resolveu esse problema e distribui a solução como parte do próprio Windows, pelo que chamar uma DLL de sistema que ela mantém, testa contra a sua própria pilha de renderização de fontes, e distribui gratuitamente a todas as máquinas custa ao HotXLS um carregamento dinâmico e um ponteiro de função; reimplementar a mesma lógica significaria possuir um analisador para um formato binário com décadas de casos extremos, para uma funcionalidade que só importa quando uma fonte calha de ser grande

Construir a lista de retenção a partir de glifos efetivamente renderizados

O HotXLS constrói a lista de retenção da criação de subconjuntos a partir de um mapa que já mantinha por outra razão, pelo que a contabilização não custa nada extra. Sempre que o código de renderização de página desenha um caractere que precisa da fonte Unicode incorporada, procura o índice de glifo desse caractere e regista o par em FUnicodeGlyphMap, uma tabela de glifo-para-ponto-de-código que também conduz o CMap ToUnicode do PDF, para que copiar e colar a partir do documento concluído devolva o texto original em vez de identificadores de glifo em bruto. Quando os fluxos de conteúdo de página estão concluídos, esse mapa já lista exatamente o conjunto de pontos de código Unicode que o documento utilizou, nem mais nem menos

var
  keepList: array of Word;
  keepCount, i: Integer;
  codePoint: LongWord;
begin
  SetLength(keepList, FUnicodeGlyphMap.Count);
  keepCount := 0;
  for i := 0 to FUnicodeGlyphMap.Count - 1 do
  begin
    codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
    if codePoint > 0 then
    begin
      keepList[keepCount] := Word(codePoint);
      Inc(keepCount);
    end;
  end;
end;

No momento da finalização, o HotXLS percorre esse mesmo mapa uma segunda vez para construir a lista de retenção que CreateFontPackage espera, um simples array dos pontos de código Unicode a reter na forma de 16 bits que o argumento de lista de retenção da API exige. Como esse argumento é um array de palavras de 16 bits, endereça o Plano Multilingue Básico de forma limpa, o que cobre CJK comum, cirílico, grego, e texto árabe sem complicação; uma folha de cálculo que se apoie em caracteres do plano suplementar, certos emojis ou escritas históricas raras, situa-se fora do que uma única entrada de lista de retenção consegue nomear diretamente, o que é uma fronteira que vale a pena conhecer, e não um defeito, uma vez que a grande maioria das folhas de cálculo empresariais ricas em Unicode nunca chega perto desse plano em primeiro lugar

O que acontece quando a fontsub.dll está ausente?

O HotXLS nunca assume que a fontsub.dll está presente, e a exportação de PDF nunca falha por ela não estar. A biblioteca é carregada dinamicamente no momento em que um subconjunto é necessário, com SafeLoadLibrary e GetProcAddress em vez de uma importação estática, precisamente porque a fontsub.dll não é uma API pública documentada e de presença garantida, ao contrário de kernel32.dll: é uma ferramenta de incorporação de fontes empacotada, e nada no contrato da Microsoft promete que sobrevive em todas as SKU, em todos os ramos de manutenção, ou em qualquer camada de compatibilidade que tente emular o Windows

var
  hFontSub: HMODULE;
  CreateFontPackage: TCreateFontPackage;
begin
  hFontSub := SafeLoadLibrary('FontSub.dll');
  if hFontSub = 0 then
    Exit; // no subsetting available - keep the full embedded font
  try
    @CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
    if not Assigned(CreateFontPackage) then
      Exit;
    // ... call CreateFontPackage, check its return code ...
  finally
    FreeLibrary(hFontSub);
  end;
end;

Todo o caminho de falha converge para o mesmo resultado. Uma DLL ausente, uma exportação ausente, um código de retorno diferente de zero, ou uma fonte cuja tabela OS/2 proíbe a criação de subconjuntos através dos seus bits de permissão de incorporação, o HotXLS limita-se a manter a fonte completa que já tinha incorporado e continua. Nada levanta uma exceção, nada aborta a exportação, e o código chamador nunca tem de envolver uma otimização de fonte no seu próprio tratamento de exceções; o PDF exportado é válido de qualquer forma, e a única variável é se acaba pequeno ou algo maior

Quanto mais pequeno fica efetivamente o PDF?

A criação de subconjuntos de fontes TrueType do HotXLS tipicamente reduz o PDF exportado de uma folha de cálculo rica em Unicode para algures entre um vigésimo e um oitavo do seu tamanho sem subconjunto, uma redução de 8 a 20 vezes cuja escala acompanha quanto de uma fonte completa um determinado documento efetivamente toca: uma nota de encomenda construída à volta de umas centenas de caracteres chineses distintos mantém apenas essas poucas centenas de glifos das dezenas de milhares que um tipo de letra CJK distribui, enquanto uma folha que abranja uma mistura mais ampla de caracteres mantém proporcionalmente mais. O HotXLS aplica ainda uma passagem adicional de compressão Flate sobre os bytes da fonte em subconjunto antes de os escrever no fluxo /FontFile2 do PDF, a mesma compressão pela qual o resto dos fluxos de conteúdo do documento já passa, e nada disto pede algo extra ao código chamador: uma folha de cálculo que nunca saia do WinAnsi nunca toca neste caminho e continua a exportar através do simples Helvetica, enquanto uma folha de cálculo que efetivamente dispare o caminho da fonte Unicode obtém a criação de subconjuntos automaticamente, sem qualquer propriedade a definir e sem qualquer chamada separada a fazer, e a única propriedade envolvida, UnicodeFontFile, apenas escolhe qual fonte é incorporada e reduzida a subconjunto, não se a criação de subconjuntos acontece

A criação de subconjuntos de fontes é um pormenor dentro da superfície mais ampla de exportação para PDF do componente Excel HotXLS para Delphi, a par da paginação, dos metadados de impressão de folha de cálculo, e dos caminhos de exportação CSV, HTML, e RTF com que é distribuído