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
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
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 /Pagevira/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
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