Artigo Técnico

Reduzir o Tamanho de PDF em Delphi: Fontes, Imagens, LZW

Para reduzir o tamanho de um ficheiro PDF em Delphi, a losLab PDF Library disponibiliza três APIs que atacam as três maiores fontes de excesso de peso: SubsetEmbeddedFonts reescreve cada programa de fonte TrueType incorporado, deixando apenas os glifos que o documento realmente representa, DownsampleImages reamostra as imagens raster que excedem um DPI alvo, e NormalizeLZWStreams substitui a compressão LZWDecode antiga por FlateDecode. Cada uma devolve o número de objetos que alterou, pelo que um zero indica que a passagem não teve efeito, em vez de ter falhado em silêncio

Porque é que o meu PDF combinado é maior do que os ficheiros de origem?

Um PDF combinado ou gerado por programa costuma ficar demasiado grande por uma de três razões: fontes incorporadas na íntegra, imagens amostradas muito acima da sua resolução de apresentação, e fluxos ainda comprimidos com o filtro LZW antigo. A norma ISO 32000-1 §9.9 permite que um produtor incorpore o programa de fonte completo, e a maioria dos produtores faz exatamente isso porque é a opção segura por omissão. Um FontFile2 completo do Arial ocupa centenas de kilobytes; incorpore-o numa dúzia de ficheiros de origem, combine-os, e passa a transportar uma dúzia de cópias de contornos de glifos para caracteres que ninguém escreveu. A combinação não cria o desperdício, apenas o concentra num único ficheiro onde o total finalmente se torna visível

As imagens são o segundo infrator. Uma digitalização com 4800 píxeis de largura colocada numa moldura de um quarto de página transporta cerca de 40 vezes mais dados de píxeis do que um fluxo de impressão a 300 DPI consegue aproveitar. O terceiro é mais discreto: os fluxos filtrados com LZWDecode. A ISO 32000-1 §7.4.4 especifica tanto LZWDecode como FlateDecode, e observa que o Flate costuma comprimir pelo menos tão bem; na prática, o resultado do Flate é sistematicamente mais pequeno sobre os mesmos dados, e o LZW sobrevive sobretudo em ficheiros que, a dada altura da sua história, passaram por ferramentas dos anos 90. O resto deste artigo percorre as três passagens da losLab PDF Library que resolvem cada problema, e depois combina-as num único pipeline

Diagrama geral que mapeia as fontes de excesso de peso de um PDF combinado para as passagens de otimização da PDF Library for Delphi aplicadas a fontes, imagens e fluxos LZW
Cada passagem visa uma fonte clássica de excesso de peso e devolve quantos objetos reescreveu, sendo que zero indica um ficheiro já enxuto e não uma falha silenciosa

Subconjuntos de fontes com SubsetEmbeddedFonts

SubsetEmbeddedFonts encolhe todas as fontes TrueType incorporadas num documento carregado, deixando apenas os caracteres que o documento efetivamente usa, e não precisa de argumentos porque deriva a lista de retenção dos próprios fluxos de conteúdo. Internamente, a passagem percorre o fluxo de conteúdo de cada página com GetTextRuns, recolhe os códigos de caracteres referenciados sob cada recurso de fonte, constrói uma lista de retenção, e entrega o programa de fonte original ao motor FontSub do Windows (CreateFontPackage) para produzir um subconjunto. O programa reescrito substitui o fluxo FontFile2 no lugar, e o nome BaseFont ganha uma etiqueta LOSABC+, a convenção de seis letras maiúsculas mais um sinal que a ISO 32000-1 §9.6.4 define para fontes em subconjunto. É esse prefixo que também torna a chamada idempotente: execute a passagem duas vezes e as fontes já reduzidas a subconjunto são reconhecidas e ignoradas, pelo que é seguro integrá-la num trabalho em lote que possa revisitar ficheiros

Pipeline da PDF Library for Delphi a mostrar como a losLab PDF Library deriva uma lista de retencao de glifos a partir das sequencias de texto e produz uma fonte em subconjunto com etiqueta atraves do motor FontSub
SubsetEmbeddedFonts percorre as sequências de texto de cada página, entrega a lista de retenção derivada ao FontSub, etiqueta os programas reescritos com LOSABC+ e ignora-os em execuções posteriores
var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = número de programas FontFile2 reescritos;
      // 0 significa nada incorporado, ou tudo já em subconjunto
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Vale a pena conhecer dois detalhes de implementação, porque explicam as fronteiras da API. Primeiro, a passagem visa o FontFile2, pelo que abrange programas TrueType incorporados; as fontes incorporadas como Type 1 ou CFF simples ficam intocadas em vez de correrem risco. Segundo, apoia-se no FontSub, o que torna SubsetEmbeddedFonts exclusivo do Windows. Um ponto mais subtil da implementação: se uma fonte se qualifica é decidido resolvendo efetivamente a cadeia de referências FontDescriptor → FontFile2, e não confiando numa heurística de sinalizador de incorporação, porque as fontes de um documento carregado nunca passaram pela contabilidade do lado da criação que define esses sinalizadores. Se o fluxo resolvido existir, a fonte é candidata; se não existir, é ignorada sem erro

O compromisso, dito com franqueza: uma fonte em subconjunto contém apenas os glifos presentes no momento em que o subconjunto foi criado. Se uma ferramenta a jusante, ou o seu próprio código, acrescentar mais tarde texto nessa mesma fonte, qualquer carácter fora do subconjunto fica sem contorno e será representado como um glifo em falta. Crie o subconjunto como último passo que altera conteúdo, nunca antes de uma fase de edição. A mesma cautela se aplica se planeia extrair a fonte mais tarde para reutilização; o artigo sobre extrair texto, imagens e fontes com a PDF Library for Delphi descreve o que um programa de fonte extraído em subconjunto lhe pode e não pode dar

Como decide o DownsampleImages que imagens reduzir?

DownsampleImages(MaxDPI, Quality, Filter) reamostra apenas as imagens que consegue classificar com confiança como sobreamostradas, usando uma estimativa de DPI deliberadamente conservadora. Um XObject de imagem PDF guarda dimensões em píxeis mas nenhuma resolução física de confiança, e qualquer etiqueta de DPI da imagem de origem raramente sobrevive a um ciclo de carregar, editar e gravar. Por isso, a passagem estima SrcDPI = PixelWidth / 8.5, perguntando na prática: se esta imagem ocupasse toda a largura de uma página Letter, qual seria a sua resolução? Só são tocadas as imagens cuja estimativa exceda MaxDPI. O enviesamento é intencional: uma imagem colocada em pequeno na página tem um DPI real superior à estimativa, pelo que a passagem age de menos em vez de degradar um recurso com qualidade de impressão que não consegue medir

Quality, de 1 a 100, seleciona a qualidade de recodificação JPEG, enquanto 0 mantém a saída como Flate sem perdas ao estilo PNG; Filter escolhe o núcleo de reamostragem, 0 para uma média de caixa e 1 para bilinear. Para papelada de escritório digitalizada, DownsampleImages(150, 75, 1) é um ponto de partida sensato; para tudo o que possa vir a ser reimpresso, suba MaxDPI para 300 ou dispense a passagem por completo. A redução de amostragem é o único passo com perdas dos três, pelo que pertence atrás de uma definição que os seus utilizadores possam desligar

Fluxo de decisao da reducao de amostragem de imagens PDF em Delphi que compara uma estimativa conservadora de SrcDPI com o MaxDPI antes de reamostrar uma imagem
Uma estimativa conservadora de DPI parte do princípio de que a imagem ocupa uma página Letter inteira, pelo que só são reamostradas as imagens sobre as quais a biblioteca tem confiança, ficando os recursos limítrofes intocados

Converter fluxos LZW antigos com NormalizeLZWStreams

NormalizeLZWStreams é o ganho gratuito: descomprime sem perdas todos os fluxos LZWDecode e volta a comprimi-los com FlateDecode, no lugar, devolvendo a contagem de fluxos convertidos. Trata tanto uma entrada única /Filter /LZWDecode como a presença de LZW dentro de um array de cadeia de filtros, onde só o elo LZW é substituído e o resto da cadeia é preservado. Os parâmetros de preditor (Predictor, Columns, Colors, BitsPerComponent) são lidos do DecodeParms do fluxo e passados ao descompressor, para que os dados de imagem codificados com preditor façam a ida e volta corretamente. Como ambos os filtros são codecs exatos ao bit, os bytes descodificados são idênticos antes e depois; só muda a compressão do contentor, e é por isso que esta passagem pode ser executada sem condições sobre qualquer ficheiro

Num documento sem fluxos LZW, a chamada limita-se a devolver 0 e não toca em nada, algo que a bateria de testes de regressão da biblioteca exercita explicitamente: um ficheiro acabado de criar apenas com Flate tem de reportar zero conversões. Essa garantia de ausência de efeito conta quando a passagem está integrada num pipeline que processa milhares de ficheiros heterogéneos, uns de 2024 e outros de 1998

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

As três passagens combinam-se numa única função de carregar, otimizar e gravar, e a ordem importa menos do que se poderia pensar, porque operam sobre tipos de objeto disjuntos: fontes, XObjects de imagem e filtros de fluxo. Executar primeiro a criação de subconjuntos continua a ser a escolha arrumada, já que é a passagem com uma restrição de ordem face à 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 -> subconjunto
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Registe Fonts/Images/Streams: três zeros indicam um ficheiro já enxuto
  finally
    Lib.Free;
  end;
end;

Verifique o pipeline como a própria biblioteca se verifica: com uma ida e volta. Os testes de regressão da v3.130 criam um documento, gravam-no, voltam a carregá-lo, executam a otimização, gravam de novo, e depois afirmam três coisas: a saída é mais pequena, as contagens devolvidas correspondem ao esperado, e um novo carregamento do ficheiro otimizado continua a ser analisado e representado. Reproduzir esse ciclo de criar, otimizar e recarregar sobre uma amostra dos seus próprios ficheiros de produção, comparando o texto extraído antes e depois, é um investimento de uma hora que apanha erros de integração muito antes de um cliente abrir uma fatura estragada

// Verificação de ida e volta: o ficheiro otimizado tem de continuar a carregar sem erros
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Onde encaixa o pipeline num fluxo de combinação? Depois da combinação, não durante. Combinar primeiro e otimizar o resultado único significa que cada fonte incorporada é reduzida a subconjunto uma só vez, contra a união de todos os caracteres usados, em vez de por ficheiro de origem. Se o débito da combinação for o estrangulamento, a PDF Library for Delphi oferece um caminho rápido ao nível dos bytes que evita a análise completa dos objetos, descrito em o artigo sobre combinação rápida de PDF com deslocamento de referências de bytes; e para entradas demasiado grandes para caber por inteiro em memória, combinar e dividir PDFs grandes com acesso direto descreve a via de streaming. Ambas se conjugam naturalmente com uma passagem final de otimização sobre o resultado combinado

O que as três passagens não fazem

O trio de otimização da losLab PDF Library exclui deliberadamente tudo o que altere a semântica do documento. SubsetEmbeddedFonts não unifica fontes duplicadas de origens combinadas num único programa, encolhe cada uma de forma independente; a desduplicação é uma transformação diferente e mais arriscada. DownsampleImages deixa passar uma imagem cuja estimativa conservadora de DPI fique abaixo do limiar, mesmo quando um humano consegue ver que está sobredimensionada para a sua moldura. E nenhuma das passagens toca na estrutura do documento, pelo que um ficheiro inchado por milhares de objetos órfãos precisa de uma gravação com reescrita completa e não destas passagens ao nível do fluxo. Dentro desses limites, a combinação de subconjuntos de fontes, redução de amostragem de imagens e normalização de LZW para Flate elimina as três fontes clássicas de excesso de peso em PDF com uma chamada de API previsível para cada uma. As três funções fazem parte da losLab PDF Library para Delphi, C# e VB.NET, a par das APIs de combinação, extração e representação discutidas acima