Ir al contenido

Reducir el tamaño de archivos PDF con compresión y subconjuntos de fuentes

El objetivo es obtener el PDF más pequeño que permita el contenido, sin pérdida de fidelidad. NextPDF ofrece dos palancas de tamaño, ambas activadas de forma predeterminada:

  • Compresión de flujos. El escritor envuelve cada flujo de contenido de página y cada programa de fuente incrustado en un flujo FlateDecode (zlib). Este ajuste se controla con la opción NextPDF\Core\Config compress. Se modifica con el wither withCompress() cuando se construye un documento en streaming.
  • Subconjuntos de fuentes. Cuando se incrusta una fuente TrueType o CFF, el escritor reconstruye el programa de fuente para que contenga solo los glifos que el documento usa realmente y después comprime el resultado con FlateDecode. Esto ocurre de forma automática: no hay ninguna opción que ajustar ni ninguna llamada que hacer. Una tipografía CJK de 20,000 glifos que aporta unos cientos de glifos a un documento se incrusta a una fracción de su tamaño en disco.

Conviene aclararlo desde el principio: NextPDF Core no expone remuestreo de imágenes, controles de calidad de imagen, conmutadores de flujos de objetos ni ajustes de desduplicación de recursos. Los únicos controles de tamaño disponibles son los dos anteriores. El resto de esta receta muestra cómo usarlos correctamente y qué no hace cada uno.

Requisitos previos: una instalación del Core (composer require nextpdf/core:^3) y, para la ruta de subconjuntos, un archivo de fuente con licencia para incrustar.

Ventana de terminal
composer require nextpdf/core:^3

Un PDF es un árbol de objetos. Los objetos más grandes suelen ser los flujos de contenido (los operadores de dibujo de cada página) y los programas de fuente (los contornos de glifos incrustados). Ambos son datos textuales y binarios muy comprimibles, por lo que la palanca de tamaño más eficaz, con diferencia, es comprimirlos con FlateDecode. FlateDecode es el nombre que la especificación PDF 2.0 da a un flujo DEFLATE envuelto en zlib (ISO 32000-2:2020 §7.4.4), y es el filtro que emite NextPDF.

El escritor fija el nivel de compresión DEFLATE en 9, el máximo de RFC 1951, mediante NextPDF\Writer\PinnedZlibCompressor. El nivel 9 intercambia un pequeño costo adicional de CPU por el flujo más pequeño. Mantenerlo fijo también hace que la salida sea determinista, porque el encabezado zlib codifica el nivel y cualquier variación cambiaría los bytes. El nivel no se elige: el motor lo fija para que dos ejecuciones sobre la misma entrada produzcan flujos idénticos byte a byte.

La segunda palanca es la creación de subconjuntos de fuentes. Un archivo de fuente en disco contiene todos los glifos que define la tipografía, pero un documento que imprime «Invoice 2026» solo necesita una docena de ellos. NextPDF\Typography\FontSubsetter (para TrueType) y NextPDF\Typography\CffSubsetter (para CFF / OpenType) recorren los puntos de código que el documento representó realmente, resuelven las dependencias de glifos compuestos y reconstruyen solo las tablas de fuente necesarias. Emiten un binario de fuente de subconjunto válido con una etiqueta de prefijo de subconjunto determinista de seis letras (ISO 32000-2:2020 §9.9). El escritor aplica este proceso siempre que conoce el conjunto de glifos usados de una fuente incrustada, y después comprime el subconjunto con FlateDecode. Si crear el subconjunto de una tipografía concreta ahorrara menos del diez por ciento, el creador de subconjuntos devuelve el programa original en su lugar, porque la sobrecarga de reconstrucción no compensa una ganancia marginal.

En síntesis: los PDF se mantienen pequeños dejando la compresión activada (el valor predeterminado) e incrustando archivos de fuente reales (para que el subconjunto tenga algo que reducir), no ajustando una larga lista de opciones.

El único control de tamaño que se ajusta está en el objeto de configuración.

NextPDF\Core\Config es un objeto de valor inmutable y final readonly con métodos wither tipados. El miembro relevante es:

  • compress (bool, predeterminado true): habilita la compresión FlateDecode. Se modifica con withCompress(bool $compress): self, que devuelve un nuevo Config con la opción cambiada y conserva todos los demás campos.

Un Config se adjunta a un documento en el momento de su construcción:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self construye un documento con registros efímeros para un script de CLI o un proceso de vida corta, aplicando el Config indicado.

Dos miembros determinan con qué pueden trabajar las palancas de tamaño, pero ninguno es en sí mismo un control de compresión:

  • imageCacheBytes (int, predeterminado 52_428_800) limita la caché de imágenes en memoria, y withImageCacheBytes(int $bytes): self la cambia. Esto acota el uso máximo de memoria durante una construcción. No remuestrea, recomprime ni reduce de ningún otro modo las imágenes que se incrustan: es un límite de memoria, no un control del tamaño de salida.
  • fontsDirectory (string) y withFontsDirectory(string $dir): self establecen la ruta de búsqueda predeterminada para los archivos de fuente, que alimenta la ruta de subconjuntos.

El trabajo con fuentes se realiza a través de la superficie de tipografía del documento:

  • setFont(string $family, string $style = '', float $size = 12.0): static selecciona una tipografía. Cuando la familia se resuelve a un archivo de fuente incrustable, el escritor registra los puntos de código que se representan para poder crear el subconjunto de esa tipografía en el momento de guardar.
  • addFontDirectory(string $directory): static registra un directorio adicional en el que se buscarán archivos de fuente.

La salida usa el trío estándar: getPdfData(): string devuelve los bytes, save(string $path): void los escribe de forma atómica, y output(?string $filename, OutputDestination $dest): string gestiona la entrega por HTTP.

La creación de subconjuntos no tiene método público ni opción. Es una propiedad que surge de incrustar una fuente y representar texto: el escritor gestiona FontSubsetter / CffSubsetter internamente dentro de NextPDF\Writer\PdfFontWriter.

Este ejemplo construye un documento con la compresión habilitada de forma explícita y una fuente incrustada con subconjunto; después escribe los bytes. Omite el manejo de errores para mostrar la forma de la llamada; el ejemplo de producción más abajo añade todas las protecciones.

<?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));

El siguiente programa es autónomo. Construye un documento con la compresión activada, incrusta una fuente desde un directorio controlado, representa texto para que el creador de subconjuntos disponga de un conjunto de glifos usados, y escribe el resultado de forma atómica. Captura las excepciones de NextPDF más específicas que generan las rutas de construcción y guardado, y después relanza cada una con contexto en lugar de silenciarla. Se debe configurar NEXTPDF_FONT_DIR para que apunte a un directorio que contenga una tipografía TrueType o CFF con licencia para incrustación; el programa valida la ruta antes de incrustar.

<?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 (el número de bytes depende de la fuente y de la generación):

Wrote <n> bytes to <path>.
  • La compresión está activada de forma predeterminada. Un Config nuevo tiene compress establecido en true. Rara vez hace falta llamar siquiera a withCompress(). Establecerlo de forma explícita solo sirve para documentar la intención o para desactivarlo en una construcción de depuración en la que se quieran leer los flujos en bruto.
  • Desactivar la compresión hace los archivos más grandes, no más pequeños. withCompress(false) es una ayuda de diagnóstico para inspeccionar flujos sin comprimir. Nunca es una optimización de tamaño. Se debe publicar con la compresión activada.
  • Los subconjuntos necesitan una fuente incrustada real. Las fuentes estándar Base14 (Helvetica, Times, Courier y sus parientes) se referencian por nombre y no incluyen ningún programa incrustado en un documento simple, así que no hay ningún programa del que crear un subconjunto. Los subconjuntos solo reducen las tipografías que se incrustan desde un archivo de fuente.
  • Los subconjuntos son automáticos y silenciosos. No hay ninguna opción, ningún método ni ninguna confirmación. Si se incrustó una fuente y se representó texto con ella, el escritor creó el subconjunto. El programa incrustado lleva una etiqueta de prefijo de subconjunto de seis letras (por ejemplo ABCDEF+LiberationSans) para que un lector pueda distinguir un subconjunto de una incrustación completa.
  • Un ahorro pequeño conserva la fuente completa. Cuando un subconjunto ahorraría menos del diez por ciento del tamaño del programa, el creador de subconjuntos devuelve el original. Es un umbral deliberado: la reconstrucción no compensa una ganancia marginal. Incrustar una tipografía que ya es diminuta, o representar casi todos sus glifos, puede caer en este caso.
  • imageCacheBytes no es un control del tamaño de las imágenes. Limita la memoria, no los bytes de salida. NextPDF Core incrusta los datos de imagen que recibe; no hay ningún paso de remuestreo, submuestreo ni recodificación. Si hacen falta imágenes más pequeñas, hay que redimensionarlas y recodificarlas antes de incrustarlas.
  • No existe ningún ajuste de flujos de objetos ni de desduplicación. NextPDF Core no expone ningún conmutador para los flujos de objetos de PDF 2.0 ni para la desduplicación de recursos. Ese control no existe: las palancas de tamaño son la compresión de flujos y los subconjuntos de fuentes.

La compresión en el nivel 9 es el costo de CPU dominante al escribir un flujo. Intercambia un pequeño porcentaje de tiempo de construcción por la salida más pequeña. El costo crece linealmente con el número de bytes sin comprimir, así que el número de páginas y la cantidad de datos de fuente incrustados fijan el presupuesto. La creación de subconjuntos añade una sola pasada por cada fuente incrustada; esa pasada analiza el directorio de tablas de la fuente, resuelve el cierre de glifos usados y reconstruye las tablas necesarias. Para una tipografía CJK grande, esta es la más cara de las dos palancas, pero se ejecuta una vez por fuente, no una vez por página. El umbral mínimo de ahorro del diez por ciento existe en parte para mantener esa pasada fuera de la ruta crítica cuando no resultaría rentable. Un documento pequeño con un subconjunto incrustado se mantiene cómodamente dentro de un límite de 1500 ms de tiempo real transcurrido y un presupuesto máximo de 96 MB. Ajustar imageCacheBytes al techo real permite que una construcción que incrusta muchas imágenes falle rápido por memoria en lugar de hacer swapping.

La generación se ejecuta en el proceso; ningún byte del documento sale del host y no se realiza ninguna llamada de red. Tratar cualquier fuente o imagen suministrada externamente como entrada no confiable:

  • Validar el directorio de fuentes. El ejemplo de producción lee la ruta de la fuente desde una variable de entorno controlada por el servidor y rechaza un directorio inexistente o ilegible antes de incrustar. Nunca se debe derivar una ruta de fuente a partir de un campo de la solicitud.
  • Incrustar solo fuentes con licencia para redistribución. Un subconjunto sigue siendo un programa de fuente incrustado. Confirmar que la licencia permite la incrustación antes de publicar un documento que lleve la tipografía.
  • Las fuentes mal formadas generan una excepción, no corrompen de forma silenciosa. Un archivo de fuente que no se puede analizar genera NextPDF\Exception\FontParsingException, y un fallo grave de zlib genera NextPDF\Exception\CompressionException. Capturar la excepción más específica y actuar en consecuencia. Nunca se debe envolver la construcción en un catch vacío.
  • Nunca interpolar entrada del usuario en la ruta de salida. El ejemplo escribe en una ruta fija o en un canal lateral controlado por el servidor, y rechaza los wrappers de flujo y los bytes nulos mediante el escritor atómico de save(). Derivar las rutas de salida a partir de valores controlados por el servidor para evitar el path traversal.
  • No incluir secretos en el documento. No incrustar credenciales, tokens ni identificadores internos en un documento generado que se devuelva a un cliente.

Esta receta no formula ninguna afirmación normativa sobre estándares por sí misma. Los mecanismos que usa están definidos por la especificación PDF 2.0: la compresión de flujos FlateDecode (ISO 32000-2:2020 §7.4.4) y la nomenclatura de subconjuntos de fuentes con un prefijo de subconjunto de seis caracteres (ISO 32000-2:2020 §9.9). NextPDF emite ambos como parte de su ruta de escritura estándar; no se configuran más allá de la opción compress. El perfil de reproducibilidad structural que declara esta página refleja que el escritor fija el nivel DEFLATE, de modo que los flujos comprimidos son deterministas, mientras que los identificadores a nivel de documento todavía pueden variar entre ejecuciones a menos que también se configuren ajustes deterministas. Para conocer los mecanismos de incrustación que hay detrás de los subconjuntos, se puede consultar la receta de incrustar y crear subconjuntos enlazada más abajo.