Pular para o conteúdo

Reduza o tamanho de arquivos PDF com compressão e subconjunto de fontes

Você quer o menor PDF que o conteúdo permitir, sem perder fidelidade. O NextPDF oferece dois controles para reduzir o tamanho do arquivo, e ambos vêm ativados por padrão:

  • Compressão de fluxos. O writer envolve cada fluxo de conteúdo de página e cada programa de fonte incorporado em um fluxo FlateDecode (zlib). O sinalizador NextPDF\Core\Config compress guarda essa configuração. Defina esse valor com o wither withCompress() ao criar um documento de streaming.
  • Subconjunto de fontes. Quando você incorpora uma fonte TrueType ou CFF, o writer reconstrói o programa da fonte para carregar apenas os glifos usados pelo documento e, em seguida, comprime o resultado com FlateDecode. Isso acontece automaticamente. Não há sinalizador a definir nem chamada a fazer. Uma face CJK de 20,000 glifos que contribui com algumas centenas de glifos para um documento é incorporada com uma fração do tamanho que teria em disco.

Um ponto de honestidade logo de início: o NextPDF Core não expõe reamostragem de imagem, controle de qualidade de imagem, alternância de fluxo de objetos nem configuração de deduplicação de recursos. Os dois controles acima são os únicos controles de tamanho. O restante desta receita mostra como usá-los corretamente e o que cada um não faz.

Pré-requisitos: uma instalação do Core (composer require nextpdf/core:^3) e, para o caminho de subconjunto, um arquivo de fonte que você esteja licenciado a incorporar.

Terminal window
composer require nextpdf/core:^3

Um PDF é uma árvore de objetos. Os maiores objetos costumam ser os fluxos de conteúdo (os operadores de desenho de cada página) e os programas de fonte (os contornos de glifos incorporados). Ambos comprimem bem; portanto, o controle de tamanho mais eficaz é comprimi-los com FlateDecode. FlateDecode é o nome em PDF 2.0 para um fluxo DEFLATE envolto em zlib (ISO 32000-2:2020 §7.4.4), e é o filtro emitido pelo NextPDF.

O writer fixa o nível de compressão DEFLATE em 9, o máximo da RFC 1951, por meio de NextPDF\Writer\PinnedZlibCompressor. O nível 9 troca um pouco mais de CPU pelo menor fluxo. Fixar o nível também mantém a saída determinística, porque o cabeçalho zlib codifica o nível, e um nível instável mudaria os bytes. Você não escolhe o nível — o mecanismo o fixa para que duas execuções com a mesma entrada produzam fluxos idênticos byte a byte.

A segunda alavanca é o subconjunto de fontes. Um arquivo de fonte em disco carrega todos os glifos que a tipografia define, mas um documento que imprime “Invoice 2026” precisa de apenas alguns deles. NextPDF\Typography\FontSubsetter (para TrueType) e NextPDF\Typography\CffSubsetter (para CFF / OpenType) percorrem os pontos de código que o documento de fato renderizou, resolvem as dependências de glifos compostos e reconstroem apenas as tabelas de fonte necessárias. Eles emitem um binário de fonte de subconjunto válido com uma tag de prefixo de subconjunto determinística de seis letras (ISO 32000-2:2020 §9.9). O writer aplica isso sempre que conhece o conjunto de glifos usados de uma fonte incorporada e, em seguida, comprime o subconjunto com FlateDecode. Se aplicar subconjunto a uma face específica economizasse menos de dez por cento, o subsetter retorna o programa original, porque o custo da reconstrução não compensa um ganho marginal.

A conclusão: você mantém os PDFs pequenos deixando a compressão ativada (o padrão) e incorporando arquivos de fonte reais (para que o subconjunto tenha algo a reduzir), não ajustando uma longa lista de opções.

O único controle de tamanho que você define fica no objeto de configuração.

NextPDF\Core\Config é um objeto de valor imutável final readonly com métodos wither tipados. O membro relacionado ao tamanho é:

  • compress (bool, padrão true) — habilita a compressão FlateDecode. Defina esse valor com withCompress(bool $compress): self, que retorna um novo Config com o sinalizador alterado e todos os demais campos preservados.

Anexe um Config a um documento no momento da construção:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self cria um documento com registros efêmeros para um script CLI ou um processo de vida curta, aplicando o seu Config.

Dois membros moldam o material sobre o qual as alavancas de tamanho atuam, mas nenhum deles é, por si só, um controle de compressão:

  • imageCacheBytes (int, padrão 52_428_800) limita o cache de imagens em memória, e withImageCacheBytes(int $bytes): self o altera. Isso limita o pico de memória durante uma construção. Ele não reamostra, recomprime nem reduz de outra forma as imagens que você incorpora — é um teto de memória, não um controle de tamanho da saída.
  • fontsDirectory (string) e withFontsDirectory(string $dir): self definem o caminho de busca padrão para arquivos de fonte, que alimenta o caminho de subconjunto.

O trabalho com fontes acontece pela superfície de tipografia no documento:

  • setFont(string $family, string $style = '', float $size = 12.0): static seleciona uma face. Quando a família resolve para um arquivo de fonte incorporável, o writer registra os pontos de código que você renderiza para poder aplicar subconjunto a essa face no momento de salvar.
  • addFontDirectory(string $directory): static registra um diretório adicional para a busca de arquivos de fonte.

A saída usa o trio padrão: getPdfData(): string retorna os bytes, save(string $path): void os grava de forma atômica, e output(?string $filename, OutputDestination $dest): string cuida da entrega via HTTP.

O subconjunto não tem método público nem sinalizador. Ele decorre da incorporação de uma fonte e da renderização de texto. O writer aciona FontSubsetter / CffSubsetter para você dentro de NextPDF\Writer\PdfFontWriter.

Este exemplo cria um documento com a compressão explicitamente ativada e uma fonte incorporada com subconjunto e, em seguida, grava os bytes. Ele omite o tratamento de erros para deixar clara a forma das chamadas. O exemplo de produção abaixo adiciona todas as proteções.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.
$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);
$doc->addFontDirectory(__DIR__ . '/fonts');
$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer
// subsets this font automatically when the document is built.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));

Este é um programa autônomo. Ele cria um documento com a compressão ativada, incorpora uma fonte de um diretório que você controla, renderiza texto para que o subsetter tenha um conjunto de glifos usados e grava o resultado de forma atômica. Ele captura as exceções mais específicas do NextPDF lançadas pelos caminhos de construção e gravação e, em seguida, relança cada uma com contexto em vez de engoli-la. Aponte NEXTPDF_FONT_DIR para um diretório que contenha uma face TrueType ou CFF que você esteja licenciado a incorporar; o programa valida o caminho antes da incorporação.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
/**
* Resolve and validate the font directory from a server-controlled source.
*
* Reading the directory from the environment keeps the path off the request
* surface. The function rejects a missing or unreadable directory so the
* embedding path never runs against untrusted or absent input.
*/
function resolveFontDirectory(): string
{
$configured = getenv('NEXTPDF_FONT_DIR');
$dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir);
if ($real === false || !is_dir($real) || !is_readable($real)) {
throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir));
}
return $real;
}
/**
* Build a compressed, font-subsetted document and return its bytes.
*
* @param non-empty-string $fontDirectory Validated directory of embeddable fonts.
*
* @return string Raw PDF bytes.
*/
function buildCompactPdf(string $fontDirectory): string
{
// compress is true by default; pin it so the intent is explicit and the
// streaming writer path honours it regardless of any wrapper defaults.
$config = (new Config())
->withCompress(true)
->withFontsDirectory($fontDirectory)
// Bound the image cache so a build cannot exhaust memory. This is a
// memory ceiling, not an output-size control.
->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config);
$doc->addFontDirectory($fontDirectory);
$doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the
// writer turns into a font subset at build time.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026', newLine: true);
$doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program
// are FlateDecode-compressed before the bytes are returned.
return $doc->getPdfData();
}
try {
$fontDirectory = resolveFontDirectory();
$pdf = buildCompactPdf($fontDirectory);
} catch (CompressionException $e) {
// Raised if the zlib encoder hard-fails while compressing a stream.
throw new RuntimeException(
sprintf('Compression failed for a %s stream.', $e->getAlgorithm()),
previous: $e,
);
} catch (InvalidConfigException $e) {
// Raised by the output path for an invalid destination configuration.
throw new RuntimeException(
sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()),
previous: $e,
);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) {
throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));
}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);

STDOUT esperado (a contagem de bytes depende da fonte e da construção):

Wrote <n> bytes to <path>.
  • A compressão vem ativada por padrão. Um Config recém-criado tem compress definido como true. Você raramente precisa de withCompress(). Defina-o explicitamente apenas para documentar a intenção ou para desativá-lo em uma construção de depuração na qual você queira ler os fluxos brutos.
  • Desativar a compressão deixa os arquivos maiores, não menores. withCompress(false) é um auxílio de diagnóstico para inspecionar fluxos não comprimidos. Nunca é uma otimização de tamanho. Publique com a compressão ativada.
  • O subconjunto precisa de uma fonte incorporada real. As fontes padrão Base14 (Helvetica, Times, Courier e suas correlatas) são referenciadas pelo nome e não carregam nenhum programa incorporado em um documento comum; portanto, não há nada a ser subconjuntado. O subconjunto só reduz faces que você incorpora a partir de um arquivo de fonte.
  • O subconjunto é automático e silencioso. Não há sinalizador, método nem confirmação. Se você incorporou uma fonte e renderizou texto com ela, o writer aplicou subconjunto a ela. O programa incorporado carrega uma tag de prefixo de subconjunto de seis letras (por exemplo, ABCDEF+LiberationSans) para que um leitor consiga distinguir um subconjunto de uma incorporação completa.
  • Uma economia pequena mantém a fonte completa. Quando um subconjunto economizaria menos de dez por cento do tamanho do programa, o subsetter retorna o original. Esse é um piso deliberado: o custo da reconstrução não compensa um ganho marginal. Incorporar uma face que já é minúscula, ou renderizar quase todos os seus glifos, pode cair nesse caso.
  • imageCacheBytes não é um controle de tamanho de imagem. Ele limita a memória, não os bytes de saída. O NextPDF Core incorpora os dados de imagem que você fornece; não há etapa de reamostragem, redução de amostragem nem recodificação. Se você precisa de imagens menores, redimensione-as e recodifique-as antes de incorporá-las.
  • Não existe configuração de fluxo de objetos nem de deduplicação. O NextPDF Core não expõe uma alternância para fluxos de objetos do PDF 2.0 nem para deduplicação de recursos. Não procure por uma — as alavancas de tamanho são a compressão de fluxos e o subconjunto de fontes.

A compressão no nível 9 é o custo de CPU dominante ao escrever um fluxo. Ela troca alguns por cento do tempo de construção pela menor saída. O custo é linear em relação à contagem de bytes não comprimidos; portanto, a contagem de páginas e a quantidade de dados de fonte incorporados definem o orçamento. O subconjunto adiciona uma passagem única por face incorporada que analisa o diretório de tabelas da fonte, resolve o fechamento de glifos usados e reconstrói as tabelas necessárias. Para uma face CJK grande, esta é a mais cara das duas alavancas, mas ela é executada uma vez por fonte, não uma vez por página. O piso de economia de dez por cento existe, em parte, para manter essa passagem fora do caminho crítico quando ela não compensaria. Um documento pequeno com um subconjunto incorporado fica confortavelmente dentro de um limite de 1500 ms de tempo de parede e de um orçamento de pico de 96 MB. Limite imageCacheBytes ao seu teto real para que uma construção que incorpora muitas imagens falhe rapidamente por falta de memória em vez de recorrer ao swap.

A construção é executada no processo; nenhum byte do documento sai do host e nenhuma chamada de rede é feita. Trate qualquer fonte ou imagem fornecida externamente como entrada não confiável:

  • Valide o diretório de fontes. O exemplo de produção lê o caminho da fonte de uma variável de ambiente controlada pelo servidor e rejeita um diretório ausente ou ilegível antes de incorporar. Nunca derive um caminho de fonte de um campo de requisição.
  • Incorpore apenas fontes que você esteja licenciado a redistribuir. Um subconjunto ainda é um programa de fonte incorporado. Confirme que a licença permite a incorporação antes de publicar um documento que carregue a face.
  • Fontes malformadas lançam exceção; elas não corrompem silenciosamente. Um arquivo de fonte que falha na análise lança NextPDF\Exception\FontParsingException, e uma falha grave do zlib lança NextPDF\Exception\CompressionException. Capture a exceção mais específica e aja sobre ela. Nunca envolva a construção em um catch vazio.
  • Nunca interpole entrada do usuário no caminho de saída. O exemplo grava em um caminho fixo ou em um canal lateral controlado pelo servidor, e rejeita stream wrappers e bytes nulos por meio do writer atômico em save(). Derive os caminhos de saída de valores controlados pelo servidor para evitar travessia de diretórios.
  • Sem segredos no documento. Não incorpore credenciais, tokens nem identificadores internos em um documento gerado que você devolva a um cliente.

Esta receita não faz nenhuma alegação normativa de padrões por conta própria. Os mecanismos que ela usa são definidos pela especificação PDF 2.0: compressão de fluxos FlateDecode (ISO 32000-2:2020 §7.4.4) e nomenclatura de subconjunto de fontes com um prefixo de subconjunto de seis caracteres (ISO 32000-2:2020 §9.9). O NextPDF emite ambos como parte do seu caminho de escrita padrão; você não os configura além do sinalizador compress. O perfil de reprodutibilidade structural que esta página declara reflete que o writer fixa o nível DEFLATE, de modo que os fluxos comprimidos são determinísticos, enquanto os identificadores em nível de documento ainda podem variar entre execuções, a menos que você também configure ajustes determinísticos. Para a mecânica de incorporação por trás do subconjunto, consulte a receita de incorporação e subconjunto referenciada abaixo.