Artigo Técnico

Reduzir o Tamanho de Arquivos PDF no Delphi: Fontes, Imagens, LZW

Para reduzir o tamanho de arquivos PDF no Delphi, a losLab PDF Library fornece três APIs que atacam as três maiores fontes de inchaço: o SubsetEmbeddedFonts grava novamente cada programa de fonte TrueType incorporada limitando-o aos glifos que o documento realmente renderiza, o DownsampleImages reamostra imagens rasterizadas que excedem um DPI de destino, e o NormalizeLZWStreams substitui a compactação antiga LZWDecode por FlateDecode. Cada função retorna o número de objetos que alterou, de modo que um retorno igual a zero informa que a passagem foi uma operação sem efeito (no-op), em vez de uma falha silenciosa

Por que meu PDF mesclado é maior que seus arquivos de origem?

Um PDF mesclado ou gerado programaticamente geralmente é superdimensionado por um de três motivos: fontes totalmente incorporadas, imagens amostradas muito acima de sua resolução de exibição e fluxos de dados ainda compactados com o filtro LZW legado. A ISO 32000-1 §9.9 permite que um produtor incorpore o programa de fonte completo, e a maioria dos produtores faz exatamente isso porque é o padrão seguro. Um FontFile2 completo da Arial chega a centenas de kilobytes; incorpore-o em uma dúzia de arquivos de origem, mescle-os e você estará carregando uma dúzia de cópias de contornos de glifos para caracteres que ninguém digitou. A mesclagem em si não cria o desperdício, apenas o concentra em um único arquivo onde o total finalmente se torna visível

As imagens são o segundo culpado. Uma digitalização de 4800 pixels de largura inserida em um quadro de um quarto de página envia cerca de 40 vezes mais dados de pixel do que um pipeline de impressão de 300 DPI pode utilizar. O terceiro é mais silencioso: fluxos filtrados com LZWDecode. A ISO 32000-1 §7.4.4 especifica tanto LZWDecode quanto FlateDecode, e observa que o Flate geralmente compacta pelo menos tão bem quanto; na prática, a saída do Flate é consistentemente menor para os mesmos dados, e o LZW sobrevive principalmente em arquivos que passaram por ferramentas da era de 1990 em algum momento de sua história. O restante deste artigo descreve as três passagens da losLab PDF Library que corrigem cada problema, combinando-as depois em um único pipeline

Criação de subconjuntos de fontes com SubsetEmbeddedFonts

O SubsetEmbeddedFonts reduz cada fonte TrueType incorporada em um documento carregado para os caracteres que o documento realmente utiliza, e não precisa de argumentos porque deriva a lista de caracteres a manter dos próprios fluxos de conteúdo. Internamente, a passagem percorre o fluxo de conteúdo de cada página com o GetTextRuns, coleta os códigos de caracteres referenciados em cada recurso de fonte, cria uma lista de retenção e entrega o programa de fonte original ao mecanismo FontSub del Windows (CreateFontPackage) para produzir um subconjunto. O programa regravado substitui o fluxo FontFile2 existente, e o nome da BaseFont ganha uma etiqueta LOSABC+, que é a convenção de seis letras maiúsculas mais o sinal de mais que a ISO 32000-1 §9.6.4 define para fontes de subconjunto. Esse prefixo também é o que torna a chamada idempotente: execute a passagem duas vezes e as fontes já processadas em subconjuntos serão reconhecidas e ignoradas, tornando seguro conectá-la a uma tarefa em lote que possa revisitar arquivos

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = number of FontFile2 programs rewritten;
      // 0 means nothing embedded, or everything already subsetted
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Um detalhe que vale a pena saber porque explica os limites da API. Primeiro, a passagem tem como alvo FontFile2, cobrindo assim programas TrueType incorporados; fontes incorporadas como Type 1 ou CFF pura são deixadas intocadas para evitar riscos. Segundo, ela depende do FontSub, o que torna o SubsetEmbeddedFonts exclusivo do Windows. Um ponto mais sutil da implementação: a qualificação de uma fonte é decidida resolvendo de fato a cadeia de referência FontDescriptorFontFile2, e não confiando em uma heurística de flag de incorporação, pois as fontes em um documento carregado nunca passaram pela escrituração do lado da criação que define essas flags. Se o fluxo resolvido existir, a fonte é uma candidata; caso contrário, ela é ignorada sem erro

A desvantagem real: uma fonte de subconjunto contém apenas os glifos presentes no momento da criação do subconjunto. Se uma ferramenta posterior, ou seu próprio código, adicionar texto na mesma fonte mais tarde, qualquer caractere fora do subconjunto não terá contorno e será renderizado como um glifo ausente. Realize a criação de subconjuntos como a última etapa de alteração de conteúdo, nunca antes de uma fase de edição. A mesma cautela se aplica se você planejar extrair a fonte novamente mais tarde para reutilização; o artigo sobre extração de texto, imagens e fontes com PDFlibPas aborda o que um programa de subconjunto extraído pode e não pode fornecer

Como o DownsampleImages decide quais imagens encolher?

O DownsampleImages(MaxDPI, Quality, Filter) reamostra apenas as imagens que pode chamar com segurança de superamostradas, usando uma estimativa de DPI deliberadamente conservadora. Um objeto de imagem PDF (image XObject) armazena dimensões de pixel, mas nenhuma resolução física confiável, e qualquer etiqueta DPI da imagem de origem raramente sobrevive a um ciclo de carregar-editar-salvar. Sendo assim, a passagem estima SrcDPI = PixelWidth / 8.5, perguntando em efeito: se esta imagem abrangesse toda a largura de uma página Carta (Letter), qual seria sua resolução? Apenas as imagens cuja estimativa exceda MaxDPI são afetadas. O viés é intencional: uma imagem posicionada de forma pequena na página tem um DPI real maior do que a estimativa, portanto a passagem é executada a menos em vez de degradar um recurso com qualidade de impressão que ela não consegue medir

A Quality de 1 a 100 seleciona a qualidade de codificação JPEG, enquanto 0 mantém a saída como Flate sem perdas do tipo PNG; o Filter escolhe o núcleo de reamostragem, sendo 0 para média de caixa (box average) e 1 para bilinear. Para documentos de escritório digitalizados, DownsampleImages(150, 75, 1) é um ponto de partida sensato; para qualquer conteúdo que possa ser reimpresso, aumente o MaxDPI para 300 ou ignore totalmente a passagem. A redução de resolução é a única etapa com perdas das três, portanto deve ficar protegida por uma configuração que seus usuários possam desativar

Convertendo fluxos LZW legados com NormalizeLZWStreams

O NormalizeLZWStreams é uma vitória garantida: ele descompacta sem perdas cada fluxo LZWDecode e o compacta novamente com FlateDecode, no próprio local, retornando a contagem de fluxos convertidos. Ele lida tanto com uma única entrada /Filter /LZWDecode quanto com o LZW que aparece dentro de uma matriz de cadeia de filtros (filter chain array), onde apenas o elo LZW é substituído e o restante da cadeia é preservado. Parâmetros do preditor (Predictor, Columns, Colors, BitsPerComponent) são lidos a partir de DecodeParms do fluxo e passados ao descompactador, de modo que os dados de imagem codificados por preditor façam a viagem de ida e volta corretamente. Como ambos os filtros são codecs exatos a nível de bit, os bytes decodificados são idênticos antes e depois; apenas a compactação do contêiner é alterada, e é por isso que esta passagem é segura para execução incondicional em cada arquivo

Em um documento sem fluxos LZW, a chamada simplesmente retorna 0 e não toca em nada, o que a suíte de regressão da biblioteca exercita explicitamente: um arquivo criado recentemente contendo apenas Flate deve relatar zero conversões. Essa garantia de no-op é importante quando a passagem faz parte de um pipeline que processa milhares de arquivos heterogêneos, alguns de 2024 e outros de 1998

O pipeline completo de otimização de tamanho no Delphi

As três passagens combinam-se em uma única função carregar-otimizar-salvar, e a ordem importa menos do que você poderia esperar, pois elas operam em tipos de objetos disjuntos: fontes, objetos de imagem XObject e filtros de fluxo (stream filters). Executar a criação de subconjuntos primeiro ainda é a escolha ideal, já que é a passagem que possui uma restrição de ordem de edição

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

Verifique o pipeline da mesma forma que a biblioteca verifica a si mesma: viagem de ida e volta (round-trip). Os testes de regressão da v3.130 criam um documento, salvam-no, recarregam-no, executam a otimização, salvam novamente e então afirmam três coisas: a saída é menor, as contagens retornadas correspondem às expectativas e o recarregamento do arquivo otimizado ainda analisa e renderiza corretamente. Reproduzir esse ciclo criar-otimizar-recarregar com uma amostra de seus próprios arquivos de produção, comparando o texto extraído antes e depois, é um investimento de uma hora que detecta erros de integração muito antes de um cliente abrir uma fatura corrompida

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Onde o pipeline se encaixa em um fluxo de trabalho de mesclagem? Após a mesclagem, não durante ela. Mesclar primeiro e otimizar o resultado único significa que cada fonte incorporada passa pela criação de subconjuntos uma única vez em relação à união de todos os caracteres usados, em vez de fazer isso por arquivo de origem. Se a vazão (throughput) de mesclagem for o gargalo, o PDFlibPas oferece um caminho rápido a nível de byte que evita a análise completa do objeto, descrito no artigo sobre mesclagem rápida de PDF com deslocamento de referência de byte; e para entradas grandes demais para serem mantidas totalmente na memória, a mesclagem e divisão de grandes PDFs com acesso direto a arquivos aborda a rota de fluxo. Ambas combinam perfeitamente com uma passagem final de otimização na saída mesclada

O que as três passagens não farão

O trio de otimização da losLab PDF Library exclui deliberadamente qualquer alteração na semântica do documento. O SubsetEmbeddedFonts não unifica fontes duplicadas em origens mescladas em um único programa, ele reduz cada uma independentemente; a eliminação de duplicatas (deduplication) é uma transformação diferente e mais arriscada. O DownsampleImages ignorará uma imagem cuja estimativa de DPI conservadora permaneça abaixo do limite, mesmo quando um humano puder perceber que ela é superdimensionada para seu quadro. E nenhuma das passagens altera a estrutura do documento, portanto um arquivo inchado por milhares de objetos órfãos precisa de um salvamento do tipo regravação, em vez destas passagens a nível de fluxo. Dentro desses limites, a combinação de criação de subconjuntos de fontes, redução de resolução de imagens e normalização de LZW para Flate remove as três fontes clássicas de inchaço do PDF com uma chamada de API previsível para cada uma. As três funções são fornecidas como parte da losLab PDF Library para Delphi, C# e VB.NET, juntamente com as APIs de mesclagem, extração e renderização discutidas acima