Artigo Técnico

HotPDF CompressDocument: font subsets compactos no Delphi

O THotPDF.CompressDocument do HotPDF é um interruptor único que faz o BeginDoc produzir o menor PDF sem perdas que o componente sabe gravar: FlateDecode no nível máximo, um cross-reference stream com object streams, font subsetting e font subsets compactos que renumeram os glyphs mantidos atrás de um /CIDToGIDMap explícito. O EndDoc então devolve as suas próprias configurações. Um documento de teste de três páginas em Arial e SimSun caiu de 10.2 MB para 20 KB com renderização idêntica

O que o CompressDocument de fato liga?

O CompressDocument sobrepõe seis configurações de gravação, mais o teto de object streams, por um documento e restaura todas depois. No BeginDoc, antes de a versão do PDF se fixar, o HotPDF registra os seus valores e define Compression como cmFlateDecode, CompressionLevel como clMaximum, liga EnableFontSubsetting e CompactFontSubsetting, e habilita UseXRefStream mais UseObjectStreams (ISO 32000-1 §7.5.7 e §7.5.8). Object streams precisam de PDF 1.5, então uma Version mais velha é elevada a 1.5 quando não está travada. PDF/A-1 proíbe as duas estruturas, então um documento PDF/A-1 mantém a tabela de cross-reference clássica e só recebe o trabalho de Flate e de fontes. Imagens ficam exatamente como você as embutiu

Diagrama do ciclo de vida do CompressDocument do HotPDF em Delphi: o BeginDoc registra os valores próprios do gravador, sobrepõe seis configurações incluindo Compression e UseObjectStreams por um documento, e o EndDoc restaura todo valor emprestado no finally mais externo dele enquanto a propriedade CompressDocument em si fica True
Seis configurações de gravação e o teto de object streams são emprestados por exatamente um documento e devolvidos quando o EndDoc roda, então um relatório que falha nunca deixa o componente preso na compressão máxima
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // aplicado pelo BeginDoc, desfeito pelo EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A restauração acontece no finally mais externo do EndDoc, então uma exceção no meio de um relatório não deixa um componente de vida longa preso na compressão máxima para o job seguinte. A propriedade CompressDocument em si continua True; só as seis configurações que ela emprestou voltam. A versão é tratada com mais cuidado. O HotPDF desfaz a elevação dele a 1.5 só se o documento ainda termina em 1.5, então quando outro recurso empurrou o arquivo para 1.6 durante a execução (uma fonte OpenType embutida, digamos), a versão maior fica, exatamente como ficaria sem compressão

Por que os font subsets continuam grandes sem compactação?

Um subset TrueType clássico descarta os outlines que você nunca desenha mas mantém cada glyph ID onde estava, e é essa numeração que o mantém pesado. O content stream mostra CIDs que igualam os GIDs originais, então o subset precisa manter um offset de loca e uma entrada de hmtx para cada slot até o glyph mais alto que ele retém, vazio ou não. Para uma fonte latina essa sobrecarga é ruído. Para uma fonte CJK como a SimSun, cujos ideógrafos sentam fundo numa tabela de glyphs muito grande, dois caracteres chineses arrastam tabelas dimensionadas para a fonte inteira. As regras de closure de font subset para glyphs com shaping decidem quais glyphs sobrevivem; compactação é sobre quanto os sobreviventes custam

O CompactFontSubsetting renumera os glyphs mantidos numa faixa densa começando em zero e grava um stream de /CIDToGIDMap no CIDFont, que a ISO 32000-1 §9.7.4.2 define como uma tabela de GIDs de dois bytes indexada por CID. Essa tabela é o truque inteiro. Content streams, o array de larguras /W e o CMap ToUnicode todos mantêm os CIDs originais, então nada do que já foi gravado precisa mudar; só a lookup de CID para glyph se move para dentro do mapa. No teste que motivou o recurso, SimSun com dois caracteres caiu de 24.8 KB de dados de fonte para 3.1 KB

Comparação entre um font subset esparso do HotPDF, que retém entradas de loca e hmtx para todo glyph ID original até o GID retido mais alto, e a saída do CompactFontSubsetting, que renumera os glyphs mantidos densamente a partir de zero e mapeia CIDs por um stream CIDToGIDMap enquanto content streams, /W e ToUnicode ficam inalterados
A renumeração move o custo para fora do programa de fonte e para dentro de um pequeno stream de mapa — dois caracteres SimSun caíram de 24.8 KB para 3.1 KB sem tocar um byte de conteúdo já gravado

A compactação tem limites firmes, e degrada em silêncio em vez de falhar. O HotPDF constrói subsets compactos só para fontes Type 0 TrueType, tanto as definidas pelo SetFont com subsetting ligado quanto a face registrada pelo RegisterUnicodeTTF. Uma fonte TrueType simples encontra os glyphs dela pelo cmap dentro do programa de fonte, que a renumeração quebraria, então ela mantém o subset esparso. Faces OpenType-CFF não têm caminho compacto também. Uma construção compacta que falha cai no subset esparso em vez de levantar exceção. A propriedade é desligada por default, então a saída existente continua idêntica byte a byte, enquanto sob PDF/A a face Unicode registrada sempre recebe um subset compacto

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // utilizável sem CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Como o gravador empacotado comprime a estrutura do arquivo?

Quando fontes e streams já estão pequenos, os dicionários e os dados de cross-reference viram o maior custo restante, então o gravador de object streams por trás do CompressDocument enxuga isso também. O guia de object streams e incremental updates cobre o formato de contêiner em si; o caminho de compressão acrescenta quatro refinamentos por cima:

  • Sintaxe compacta pela ISO 32000-1 §7.2.2: um espaço é gravado só entre dois tokens que caso contrário correriam juntos como caracteres regulares, então /Type /Page vira /Type/Page
  • Os campos do cross-reference stream assumem qualquer largura que a §7.5.8.2 permite, então um arquivo abaixo de 16 MB guarda cada offset em 3 bytes em vez de 4
  • Até 250 objetos entram em cada object stream em vez dos 100 usuais, a menos que você defina o próprio teto pelo ConfigureAdaptiveObjectStreamPacking
  • Quando o arquivo não está criptografado, o Catalog e o dicionário Info também são empacotados em object streams; saída criptografada os mantém no nível de topo

A sintaxe compacta veio com uma armadilha que vale conhecer se você estende o gravador. A assinatura preenche a assinatura depois de o arquivo ser gravado buscando nos bytes os placeholders literais /ByteRange ( e /Contents <, e a grafia compacta transformaria isso em /ByteRange( e /Contents<, que a busca nunca encontra. Dicionários de assinatura (Type Sig ou DocTimeStamp, FT Sig) e o dicionário de criptografia portanto mantêm o layout espaçado. Um defeito relacionado afetava builds antes da v2.766.41: todo save com object streams, CompressDocument incluído, começava com duas linhas de header %PDF-, então faça upgrade se um validador estrito sinalizar a sua saída

Dá para comprimir um PDF que já está carregado?

Sim, pela sobrecarga de opções CompressLoadedDocument(Options, Info), que roda os mesmos passos sem perdas num arquivo existente. Com THPDFLoadedDocumentCompressionOptions.Default ela remove recursos de página não usados, mescla fontes e forms idênticos, faz subsetting de fontes embutidas com subsets compactos ligados, recomprime streams sem filtro, Flate, LZW, ASCII e RunLength com Flate quando o resultado é menor, e faz o próximo save usar object streams. O HighRatioFlate fica desligado por default, e object streams são pulados para PDF/A-1 e saves incrementais. A sobrecarga sem parâmetros do CompressLoadedDocument é a chamada mais antiga e mais estreita, que só comprime streams sem compressão com Flate

Fluxo do CompressLoadedDocument do HotPDF em Delphi: a chamada remove recursos de página não usados, mescla fontes e forms idênticos, faz subsetting de fontes embutidas com subsets compactos, recomprime streams com Flate só quando o resultado é menor, e liga object streams para o próximo save, enquanto signature fields disparam RefusedBySignaturePolicy e deixam o arquivo intocado
Cada passo reescreve bytes que uma assinatura cobre, então o documento inteiro é recusado a menos que você permita invalidação explicitamente — o Info.BytesSaved então soma só o trabalho de recursos, fontes e streams
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Duas fronteiras importam no caminho carregado. Cada passo reescreve bytes que uma assinatura cobre, então um documento com signature fields é recusado como um todo: a chamada retorna 0, define RefusedBySignaturePolicy e não muda nada, a menos que você defina AllowSignatureInvalidation, depois do que o Info.SignaturesInvalidated diz o que você abriu mão. A compactação também é mais conservadora aqui que no caminho de criação. O HotPDF compacta só programas de fonte usados exclusivamente por fontes CIDFontType2 com um /CIDToGIDMap Identity, onde CID iguala GID, e pula programas com um stream de mapa existente, um /CIDSet, ou tabelas de glyphs de cor como COLR, sbix, CBDT ou SVG, porque a reconstrução compacta descartaria as camadas de cor. Note também que o Info.BytesSaved soma só os passos de recursos, fontes e streams; o ganho de object streams aparece quando o arquivo é gravado

Que resultados esperar na prática?

Os ganhos acompanham quanto do arquivo é estrutura sem compressão e dados de fonte acima do tamanho, não quantas páginas ele tem. A amostra de três páginas em Arial e SimSun encolheu de 10.2 MB para 20 KB quando gerada com CompressDocument, e de 10.2 MB para 19.8 KB quando o original sem compressão foi carregado e passado pelo CompressLoadedDocument, com renderização idêntica nos dois caminhos. Um PDF que já é compacto mal se move: no conjunto de regressão, esses arquivos salvaram entre -0.07% e +0.06% do tamanho original. Arquivos pesados em fotos ganham pouco, porque nenhum dos caminhos toca dados de imagem

Se você gera os mesmos relatórios CJK toda noite, emparelhe subsets compactos com o cache persistente de font subsets em disco para o trabalho de subsetting não se repetir a cada execução, e faça diff de saídas comprimidas pelo conteúdo de objeto em vez de por bytes, já que um campo alterado re-Flate um object stream inteiro. A referência completa de propriedades e records está na página de produto do componente HotPDF Delphi PDF