Duas faturas geradas do mesmo modelo com a mesma face TrueType, diferindo apenas no sobrenome do cliente, deixam dois arquivos separados em um cache de subsets de fontes. O sobrenome acrescenta um Q maiúsculo e um z minúsculo que a primeira fatura nunca usou, então o conjunto de glifos mantidos muda, e com ele o subset e a chave de cache. Um cache persistente de subsets compensa quando o repertório de glifos dos seus documentos se repete, e os contadores que ele expõe dizem, dentro de um único lote, se o seu repete
Ativando o cache em disco no HotPDF
O HotPDF mantém o cache de subsets desligado até você dar uma pasta a ele. Duas propriedades do THotPDF o controlam: FontSubsetCacheFolder nomeia o diretório, e FontSubsetCacheMaxBytes limita quanto ele pode guardar, com padrão de 268435456 bytes (256 MiB). O cache só fica ativo enquanto a pasta não está vazia e o limite é maior que zero. Atribuir a pasta cria o diretório, se ele não existir, e imediatamente apara o que já está lá para dentro do limite
O subsetting vale cache porque é trabalho de verdade. Construir um subset TrueType significa percorrer o fechamento de glifos compostos, reescrever loca e glyf, reconstruir as tabelas que indexam glifos e produzir um mapeamento CID que o PDF consegue endereçar. Para um documento, o custo se perde no ruído. Em um servidor de relatórios que renderiza milhares de extratos em loop com as mesmas três faces, os mesmos subsets são construídos repetidamente, e essa é a CPU que o cache devolve
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// caminho só ASCII: a propriedade é um AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB em vez do padrão de 256 MiB
// ... BeginDoc / desenho / EndDoc para cada documento do lote ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt é a sua própria rotina de log
LogFmt('subset cache: %d hits, %d misses, %d bytes in %d files',
[Info.HitCount, Info.MissCount, Info.CurrentBytes, Info.FileCount]);
finally
Pdf.Free;
end;
end;
O cache fica atrás dos dois caminhos de subsetting: faces subsetadas via EnableFontSubsetting, que cobre fontes TrueType e OpenType-CFF, e a face registrada com RegisterUnicodeTTF. Nada mais no seu código de desenho muda. O caminho da pasta é declarado como AnsiString, então um nome de diretório com caracteres fora da code page do sistema é pedir para ter problemas; um caminho ASCII simples sob ProgramData ou em um diretório de dados de serviço elimina a questão por completo
O que entra em uma chave de cache
Quatro entradas decidem se um subset armazenado pode ser reutilizado: os bytes exatos da fonte de origem, o conjunto de glifos que o documento mantém, o modo de subset e a versão do formato de cache. Cada chave é construída a partir de um hash SHA-256 dos bytes da fonte e um hash SHA-256 de um bitmap dos IDs de glifo usados, prefixado com v1- e com sufixo -s para um subset esparso ou -c para um compacto. As entradas chegam ao disco como <key>.hpdfsubset, então uma listagem do diretório mostra de relance qual modo o seu lote está produzindo
Fazer o hash do conteúdo da fonte em vez do nome do arquivo tem uma consequência prática. Atualizar a Arial ou trocar por um build mais novo de uma fonte corporativa produz novas chaves automaticamente, e as entradas antigas simplesmente deixam de ser acessadas até a eviction removê-las. Você não precisa limpar o cache depois de uma atualização de fonte para obter saída correta. Limpar só recupera o espaço mais cedo
O modo de subset é a entrada que as pessoas esquecem. O CompactFontSubsetting renumera os glifos mantidos em uma faixa densa e escreve um /CIDToGIDMap explícito (ISO 32000-1 §9.7.4.2), e a face registrada sempre recebe um subset compacto quando o PDFACompliance está definido. O CompressDocument também liga os subsets compactos durante a vigência do documento. Assim, um serviço que grava o mesmo extrato uma vez como PDF simples e outra como PDF/A preenche dois conjuntos de entradas para glifos idênticos, um terminando em -s e outro em -c. Faces OpenType-CFF sempre seguem o caminho esparso
O conjunto de glifos é onde a taxa de acerto se decide. Extratos de um modelo fixo, formulários cujos dados variáveis são na maioria dígitos, catálogos desenhados a partir de um único banco de produtos: esses reutilizam os mesmos glifos, documento após documento. Uma carga de trabalho em que cada documento puxa uma fatia diferente de uma face CJK com dezenas de milhares de glifos quase sempre erra, porque o bitmap de quase todo documento é novo. Rode um lote representativo e leia os contadores antes de decidir que tipo de carga você tem
Como saber se o cache está fazendo alguma coisa?
O GetFontSubsetCacheInfo retorna um registro THPDFFontSubsetCacheInfo com nove campos, e o HitCount contra o MissCount responde à pergunta diretamente. WriteCount e EvictionCount mostram se as entradas vivem tempo suficiente para serem reutilizadas ou são empurradas para fora por um limite apertado demais. CurrentBytes e FileCount descrevem a pasta como ela está agora
Dois detalhes importam ao ligar esses contadores ao monitoramento. Os sete contadores de eventos pertencem à instância do THotPDF e começam em zero quando o componente é criado, então um serviço que cria um componente por documento vê, por leitura, a atividade de um documento. CurrentBytes e FileCount, em contraste, vêm da enumeração da pasta no momento da chamada, o que os faz refletir todos os processos que usam essa pasta e também faz cada chamada custar uma varredura de diretório. Leia o registro uma vez por lote, no final, e mantenha o loop por documento livre dele
Os três contadores restantes são os que merecem um alerta. O CorruptCount conta as entradas que falharam na validação ao carregar e foram apagadas; cada uma também conta como erro. Meia dúzia depois de uma queda de energia é esperado, e um gotejar constante aponta para o armazenamento. O RejectedCount conta os subsets que o cache recusou armazenar, tipicamente porque um único subset é maior que o limite inteiro. O WriteFailureCount conta gravações que não completaram, o que na prática é quase sempre uma pasta em que a conta de serviço não pode escrever. Nenhum dos três interrompe a geração de documentos, então um cache com problema de permissão de escrita produz exatamente os mesmos PDFs que nenhum cache. Ele simplesmente nunca fica mais rápido
// SubsetCacheNeedsAttention é um helper que você mesmo adiciona;
// os limiares são exemplos, ajuste-os ao tamanho do seu lote
function SubsetCacheNeedsAttention(const Info: THPDFFontSubsetCacheInfo;
out Reason: string): Boolean;
begin
Reason := '';
if Info.WriteFailureCount > 0 then
Reason := 'cache folder is not writable for this account'
else if Info.RejectedCount > Info.WriteCount then
Reason := 'subsets exceed FontSubsetCacheMaxBytes, raise the budget'
else if (Info.EvictionCount > Info.WriteCount div 2) and (Info.HitCount < Info.MissCount) then
Reason := 'entries are evicted before they are reused'
else if Info.CorruptCount > 10 then
Reason := 'repeated damaged entries, check the storage';
Result := Reason <> '';
end;
Limites, eviction e a chave que esvazia a pasta
A eviction é least-recently-used pelo horário de modificação do arquivo. Todo carregamento bem-sucedido toca o timestamp da entrada, toda gravação é seguida de uma apara, e os arquivos mais antigos vão primeiro até a pasta caber no limite de novo. Reduzir o FontSubsetCacheMaxBytes em tempo de execução apara na hora em vez de esperar a próxima gravação, então um serviço reagindo a um alerta de espaço em disco pode encolher o cache no momento em que decidir
Essa imediaticidade tem um fio cortante. Definir o limite como zero com uma pasta configurada apara até zero, o que apaga todo arquivo .hpdfsubset dela. Definir o FontSubsetCacheFolder como string vazia, por outro lado, desliga o cache e deixa os arquivos exatamente onde estão. As duas chaves desativam o cache para o próximo documento; só uma delas mantém uma semana de entradas quentes. Para diagnosticar, esvazie a propriedade da pasta e deixe o limite em paz
// Pausa o cache para uma execução de diagnóstico, mantém cada entrada armazenada
Pdf.FontSubsetCacheFolder := '';
// Perigo: com uma pasta configurada, um limite zero apara a pasta até nada
// Pdf.FontSubsetCacheMaxBytes := 0;
// Reset deliberado: apaga *.hpdfsubset e temporários que ficaram para trás,
// os contadores de eventos mantêm seus valores
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Uma execução de diagnóstico com o cache pausado é a forma mais limpa de eliminá-lo da suspeita. A tag de subset de seis letras no /BaseFont é derivada do conjunto de glifos mantidos, então os mesmos glifos produzem a mesma tag com ou sem o cache, e os programas de fonte incorporados devem bater. Hashes do arquivo inteiro ainda vão diferir por causa das datas de criação e do ID do documento, então compare os streams de fonte, não os arquivos
O que acontece com uma entrada danificada ou escrita pela metade?
Uma entrada danificada é apagada e o subset é construído normalmente, antes que qualquer coisa dessa entrada alcance um stream de PDF. Ao carregar, o HotPDF verifica a assinatura do arquivo, a versão do formato, a flag de modo de subset, a contagem de glifos contra a fonte de origem, os comprimentos declarados contra o tamanho real do arquivo e o cabeçalho sfnt do programa de fonte armazenado. Qualquer divergência descarta a entrada, incrementa o CorruptCount e cai no subsetting em memória. Colocar um programa de fonte quebrado no PDF de um cliente apareceria semanas depois em um visualizador na máquina de outra pessoa, longe da causa — e essa é a falha que essa validação existe para evitar
Entradas escritas pela metade nem chegam a existir. Uma gravação escreve em um arquivo temporário cujo nome carrega o ID do processo e a instância do componente, e então o move sobre o nome final com um único rename de substituição e flush. Uma queda no meio da escrita deixa um arquivo .tmp- perdido, que o ClearFontSubsetCache varre, e a entrada real ou é a versão antiga completa ou a nova completa. Entradas compactas também guardam o mapa de glifos antigo-para-novo ao lado do programa de fonte, então um subset compacto em cache reconstrói o mesmo /CIDToGIDMap e a saída PDF/A pode usar o cache sem contorná-lo
Onde a pasta deve ficar?
Três condições decidem: a conta de serviço precisa poder escrever lá, a pasta deve estar em armazenamento local, e nenhum passo de implantação deve apagá-la. Um compartilhamento de rede transforma cada consulta em uma ida e volta e entrega a semântica de rename ao servidor de arquivos. Uma pasta dentro do diretório da aplicação que o instalador recria significa que o cache começa frio após cada atualização, o que apaga silenciosamente o benefício justamente no dia em que você mais observa o desempenho
Vários componentes e vários processos podem compartilhar uma pasta local. Nomes temporários nunca colidem, o rename final é atômico, e perder uma corrida não custa nada pior que um erro ou uma gravação que precisa ser repetida no próximo documento. Compartilhar é o padrão melhor em uma máquina, porque cada instância aquece o cache para as outras. O único efeito colateral visível é que os contadores de cada instância descrevem apenas o tráfego dela, enquanto CurrentBytes e FileCount descrevem o de todos, e um tick ocasional do WriteFailureCount pode vir de dois processos substituindo a mesma entrada ao mesmo tempo. Uma pasta somente leitura ainda serve as entradas existentes; só não consegue atualizar os timestamps delas nem adicionar novas
Quando o cache é a alavanca errada
O cache remove trabalho repetido. Ele não faz nada pelo primeiro documento de um lote e pouco por cargas cujos conjuntos de glifos nunca se repetem. Se a saída é dominada por uma face CJK grande usada em texto imprevisível, olhe quais glifos são puxados para cada subset e por quê — coberto nas notas sobre fechamento de subset de fontes e formação de glifos. Se o objetivo é tamanho de arquivo em vez de CPU, o artigo sobre CompressDocument e subsets compactos de fontes explica o que o modo compacto muda na fonte incorporada. Quando um lote se mostra lento por razões sem relação com fontes, o passo a passo de saída de relatórios com fontes e imagens mostra para onde o resto do tempo normalmente vai, e o texto sobre ordenação de subsets de fontes no EndDoc cobre quando os subsets são construídos durante a finalização do documento
Como o cache de subsets vive dentro do componente, um servidor de relatórios em Delphi ou C++Builder o adota definindo um caminho de pasta e um limite, sem serviço extra para implantar ou monitorar. Incorporação de fontes, subsetting e o restante dos recursos de geração de PDF estão listados na página do componente Delphi da HotPDF