Artigo Técnico

Reduzir o Tamanho de Ficheiro PDF no Delphi: Tipos de Letra, Imagens, LZW

Para reduzir o tamanho de ficheiros PDF no Delphi, a losLab PDF Library disponibiliza três APIs que atacam as três maiores fontes de excesso de tamanho: SubsetEmbeddedFonts reescreve cada programa de tipo de letra TrueType incorporado para incluir apenas os glifos que o documento realmente renderiza, DownsampleImages reamostra imagens rasterizadas que excedem um DPI de destino, e NormalizeLZWStreams substitui a compressão LZWDecode legada por FlateDecode. Cada uma devolve o número de objetos que alterou, pelo que um zero indica-lhe que a passagem não teve qualquer efeito (no-op) em vez de ser uma falha silenciosa

Por que razão o meu PDF fundido é maior do que os seus ficheiros de origem?

Um PDF fundido ou gerado programaticamente é geralmente excessivamente grande por uma de três razões: tipos de letra totalmente incorporados, imagens amostradas muito acima da sua resolução de exibição e fluxos ainda comprimidos com o filtro LZW legado. A norma ISO 32000-1 §9.9 permite que um gerador incorpore o programa completo do tipo de letra, e a maioria dos geradores faz exatamente isso porque é a opção padrão segura. Um FontFile2 do Arial completo ocupa centenas de kilobytes; incorpore-o numa dúzia de ficheiros de origem, funda-os e passará a carregar uma dúzia de cópias de contornos de glifos para carateres que ninguém digitou. A fusão em si 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 culpado. Uma digitalização com 4800 píxeis de largura colocada numa moldura de um quarto de página envia cerca de 40 vezes mais dados de píxeis do que um fluxo de impressão de 300 DPI consegue utilizar. O terceiro é mais silencioso: fluxos filtrados com LZWDecode. A norma ISO 32000-1 §7.4.4 especifica LZWDecode e FlateDecode, e observa que o Flate geralmente comprime pelo menos tão bem; na prática, a saída Flate é consistentemente menor com os mesmos dados, e o LZW sobrevive principalmente em ficheiros que passaram por ferramentas da década de 1990 em algum momento da sua história. O resto deste artigo descreve as três passagens da losLab PDF Library que corrigem cada problema e, em seguida, combina-as num único fluxo de processamento

Subsetting de tipos de letra com o SubsetEmbeddedFonts

O SubsetEmbeddedFonts reduz cada tipo de letra TrueType incorporado num documento carregado para os carateres que o documento realmente utiliza, e não necessita de argumentos porque obtém a lista de retenção a partir dos próprios fluxos de conteúdo. Internamente, a passagem percorre o fluxo de conteúdo de cada página com o GetTextRuns, recolhe os códigos de carateres referenciados em cada recurso de tipo de letra, constrói uma lista de retenção e entrega o programa de tipo de letra original ao motor FontSub do Windows (CreateFontPackage) para produzir um subconjunto. O programa reescrito substitui o fluxo FontFile2 no local, e o nome BaseFont ganha uma etiqueta LOSABC+, a convenção de seis letras maiúsculas mais o sinal de mais definida pela norma ISO 32000-1 §9.6.4 para tipos de letra de subconjunto. Esse prefixo é também o que torna a chamada idempotente: execute a passagem duas vezes e os tipos de letra já submetidos a subsetting são reconhecidos e ignorados, pelo que integrá-la num trabalho em lote que possa reanalisar ficheiros é seguro

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;

Vale a pena conhecer dois detalhes de implementação porque explicam os limites da API. Primeiro, a passagem tem como alvo o FontFile2, cobrindo assim os programas TrueType incorporados; os tipos de letra incorporados como Type 1 ou CFF simples são deixados intocados para evitar riscos. Segundo, depende do FontSub, o que torna o SubsetEmbeddedFonts exclusivo para Windows. Um ponto mais subtil da implementação: se um tipo de letra é elegível é decidido resolvendo efetivamente a cadeia de referências FontDescriptorFontFile2, e não confiando numa heurística de flag de incorporação, porque os tipos de letra num documento carregado nunca passaram pela organização do lado da criação que define essas flags. Se o fluxo resolvido existir, o tipo de letra é um candidato; caso contrário, é ignorado sem erro

A contrapartida honesta: um tipo de letra de subconjunto contém apenas os glifos presentes no momento do subsetting. Se uma ferramenta a jusante, ou o seu próprio código, adicionar posteriormente texto com esse mesmo tipo de letra, qualquer caráter fora do subconjunto não terá contorno e será renderizado como um glifo em falta. Faça o subsetting como a última etapa de alteração de conteúdo, nunca antes de uma fase de edição. A mesma precaução aplica-se se planear extrair novamente o tipo de letra mais tarde para reutilização; o artigo sobre extração de texto, imagens e tipos de letra com o PDFlibPas aborda o que um programa de subconjunto extraído pode e não pode fornecer-lhe

Como é que o DownsampleImages decide quais as imagens a reduzir?

O DownsampleImages(MaxDPI, Quality, Filter) reamostra apenas as imagens que pode considerar com confiança estarem sobreamostradas, utilizando uma estimativa de DPI deliberadamente conservadora. Um PDF image XObject armazena dimensões de píxeis, mas nenhuma resolução física fiável, e qualquer etiqueta de DPI da imagem original raramente sobrevive a um ciclo de carregar-editar-gravar. Por isso, a passagem estima SrcDPI = PixelWidth / 8.5, perguntando na prática: se esta imagem abrangesse a largura total de uma página Letter, qual seria a sua resolução? Apenas as imagens cuja estimativa excede MaxDPI são afetadas. O desvio é intencional: uma imagem colocada com tamanho reduzido na página tem um DPI real superior à estimativa, pelo que a passagem falha por defeito em vez de degradar um recurso com qualidade de impressão que não consegue medir

O parâmetro Quality de 1 a 100 seleciona a qualidade de recodificação JPEG, enquanto 0 mantém a saída como Flate sem perdas (estilo PNG); o Filter escolhe o kernel de reamostragem, sendo 0 para média de caixa (box average) e 1 para bilinear. Para documentação de escritório digitalizada, DownsampleImages(150, 75, 1) é um ponto de partida sensato; para tudo o que possa ser reimpresso, aumente o MaxDPI para 300 ou ignore totalmente a passagem. O downsampling é a única etapa com perdas das três, pelo que deve estar associado a uma configuração que os seus utilizadores possam desativar

Converter fluxos LZW legados com o NormalizeLZWStreams

O NormalizeLZWStreams é o ganho gratuito: descomprime sem perdas cada fluxo LZWDecode e volta a comprimi-lo com FlateDecode, no local, devolvendo a contagem de fluxos convertidos. Trata tanto uma única entrada /Filter /LZWDecode como a presença de LZW dentro de um array de cadeia de filtros, onde apenas a ligação LZW é substituída e o resto da cadeia é preservado. Os parâmetros de predição (Predictor, Columns, Colors, BitsPerComponent) are lidos a partir de DecodeParms do fluxo e transmitidos para o descompressor, garantindo que os dados de imagem codificados por predição efetuem a conversão corretamente. Como ambos os filtros são codecs exatos ao nível do bit, os bytes descodificados são idênticos antes e depois; apenas a compressão do contentor muda, razão pela qual esta passagem é segura para ser executada incondicionalmente em todos os ficheiros

Num documento sem fluxos LZW, a chamada simplesmente devolve 0 e não toca em nada, o que o conjunto de testes de regressão da biblioteca exercita explicitamente: um ficheiro apenas Flate recém-criado deve reportar zero conversões. Essa garantia de no-op é importante quando a passagem se encontra num fluxo de processamento que lida com milhares de ficheiros heterogéneos, alguns de 2024 e outros de 1998

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

As três passagens combinam-se numa única função carregar-otimizar-gravar, e a ordem importa menos do que possa esperar porque funcionam em tipos de objetos disjuntos: tipos de letra, image XObjects e filtros de fluxo. Executar primeiro o subsetting continua a ser a escolha mais limpa, visto ser a passagem com 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 fluxo de processamento da mesma forma que a biblioteca se verifica a si própria: ida e volta (round-trip). Os testes de regressão da v3.130 criam um documento, gravam-no, recarregam-no, executam a otimização, gravam novamente e, em seguida, afirmam três coisas: a saída é menor, as contagens devolvidas correspondem às expectativas e o recarregamento do ficheiro otimizado continua a analisar e renderizar corretamente. Reproduzir esse ciclo de criar-otimizar-recarregar com uma amostra dos seus próprios ficheiros de produção e comparar o texto extraído antes e depois é um investimento de uma hora que deteta 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 se enquadra o fluxo de processamento num fluxo de fusão? Após a fusão, e não durante a mesma. Fundir primeiro e otimizar o resultado único significa que cada tipo de letra incorporado é submetido a subsetting uma única vez em relação à união de todos os carateres utilizados, em vez de por ficheiro de origem. Se a velocidade de fusão for o obstáculo, o PDFlibPas disponibiliza um caminho rápido ao nível dos bytes que evita a análise completa do objeto, descrito no artigo sobre fusão rápida de PDF com deslocamento de referências de bytes; e para entradas demasiado grandes para serem mantidas totalmente em memória, o acesso direto a fusão e divisão para PDFs grandes cobre a via de streaming. Ambos combinam perfeitamente com uma passagem final de otimização na saída fundida

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

O trio de otimização da losLab PDF Library exclui deliberadamente qualquer alteração da semântica do documento. O SubsetEmbeddedFonts não unifica tipos de letra duplicados entre origens fundidas num único programa, reduz cada um de forma independente; a eliminação de duplicados é uma transformação diferente e mais arriscada. O DownsampleImages ignorará uma imagem cuja estimativa conservadora de DPI permaneça abaixo do limite, mesmo quando um ser humano possa notar que está sobredimensionada para a sua moldura. E nenhuma das passagens afeta a estrutura do documento, pelo que um ficheiro inflacionado por milhares de objetos órfãos necessita de uma gravação estilo reescrita em vez destas passagens ao nível do fluxo. Dentro desses limites, la combinação de subsetting de tipos de letra, downsampling de imagens e normalização de LZW para Flate remove as três fontes clássicas de excesso de tamanho em PDF com uma única chamada de API previsível para cada. 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 fusão, extração e renderização discutidas acima