Artículo técnico

Reducir el tamaño de archivo PDF en Delphi: Fuentes, Imágenes, LZW

Para reducir el tamaño de los archivos PDF en Delphi, losLab PDF Library proporciona tres API que atacan las tres fuentes principales de exceso de tamaño: SubsetEmbeddedFonts vuelve a escribir cada programa de fuentes TrueType incrustado reduciéndolo a los glifos que el documento realmente representa, DownsampleImages vuelve a muestrear las imágenes rasterizadas que superan los PPP (DPI) objetivo y NormalizeLZWStreams reemplaza la compresión LZWDecode heredada con FlateDecode. Cada una devuelve el número de objetos que modificó, por lo que un cero indica que la pasada no tuvo efecto en lugar de ser un fallo silencioso

¿Por qué mi PDF fusionado es más grande que sus archivos de origen?

Un PDF fusionado o generado mediante programación suele tener un tamaño excesivo por una de tres razones: fuentes completamente incrustadas, imágenes muestreadas muy por encima de su resolución de pantalla y flujos aún comprimidos con el filtro LZW heredado. La norma ISO 32000-1 §9.9 permite que un productor incruste el programa de fuentes completo, y la mayoría de los productores hacen exactamente eso porque es la opción predeterminada segura. Un FontFile2 completo de Arial ocupa cientos de kilobytes; incrústelo en una docena de archivos de origen, fusiónelos y estará transportando una docena de copias de esquemas de glifos para caracteres que nadie escribió. La fusión en sí misma no crea el desperdicio, solo lo concentra en un único archivo donde el total finalmente se hace visible

Las imágenes son el segundo infractor. Un escaneo de 4800 píxeles de ancho colocado en un marco de un cuarto de página transmite aproximadamente 40 veces más datos de píxeles de los que puede utilizar una canalización de impresión de 300 PPP. El tercero es más silencioso: flujos filtrados con LZWDecode. La norma ISO 32000-1 §7.4.4 especifica tanto LZWDecode como FlateDecode, y señala que Flate generalmente comprime al menos tan bien; en la práctica, la salida de Flate es sistemáticamente más pequeña para los mismos datos, y LZW sobrevive principalmente en archivos que pasaron por herramientas de la década de 1990 en algún momento de su historia. El resto de este artículo analiza las tres pasadas de losLab PDF Library que solucionan cada problema y luego las combina en una única canalización

Subdivisión de fuentes con SubsetEmbeddedFonts

SubsetEmbeddedFonts reduce cada fuente TrueType incrustada en un documento cargado a los caracteres que el documento realmente utiliza, y no necesita argumentos porque deriva la lista de mantenimiento a partir de los propios flujos de contenido. Internamente, la pasada recorre el flujo de contenido de cada página con GetTextRuns, recopila los códigos de caracteres a los que se hace referencia bajo cada recurso de fuente, crea una lista de mantenimiento y entrega el programa de fuentes original al motor FontSub de Windows (CreateFontPackage) para producir un subconjunto. El programa reescrito reemplaza el flujo FontFile2 en su lugar, y el nombre BaseFont obtiene una etiqueta LOSABC+, la convención de seis letras mayúsculas más el signo más que la norma ISO 32000-1 §9.6.4 define para fuentes subdivididas. Ese prefijo es también lo que hace que la llamada sea idempotente: ejecute la pasada dos veces y las fuentes que ya han sido subdivididas serán reconocidas y omitidas, por lo que integrarlo en un trabajo por lotes que pueda volver a examinar archivos es seguro

Vale la pena conocer dos detalles de implementación porque explican los límites de la API. Primero, la pasada se dirige a FontFile2, por lo que cubre programas TrueType incrustados; las fuentes incrustadas como Type 1 o CFF básico se dejan intactas en lugar de arriesgarse. Segundo, depende de FontSub, lo que hace que SubsetEmbeddedFonts sea exclusivo de Windows. Un punto más sutil de la implementación: si una fuente califica se decide resolviendo realmente la cadena de referencia FontDescriptorFontFile2, no confiando en una heurística de bandera incrustada, porque las fuentes en un documento cargado nunca pasaron por la contabilidad del lado de la creación que establece tales banderas. Si el flujo resuelto existe, la fuente es candidata; de lo contrario, se omite sin error

El compromiso real: una fuente subdividida contiene solo los glifos presentes en el momento de la subdivisión. Si una herramienta descendente, o su propio código, añade más tarde texto en esa misma fuente, cualquier carácter fuera del subconjunto no tendrá contorno y se representará como un glifo faltante. Subdivida como el último paso de cambio de contenido, nunca antes de una etapa de edición. La misma precaución se aplica si planea extraer la fuente más tarde para reutilizarla; el artículo sobre la extracción de texto, imágenes y fuentes con PDFlibPas cubre lo que un programa de subconjunto extraído puede y no puede darle

¿Cómo decide DownsampleImages qué imágenes reducir?

DownsampleImages(MaxDPI, Quality, Filter) vuelve a muestrear solo las imágenes que puede clasificar con confianza como sobremuestreadas, utilizando una estimación de PPP deliberadamente conservadora. Un XObject de imagen PDF almacena dimensiones de píxeles pero ninguna resolución física confiable, y cualquier etiqueta de PPP de la imagen de origen rara vez sobrevive a un ciclo de carga-edición-guardado. Por lo tanto, la pasada estima SrcDPI = PixelWidth / 8.5, preguntando en efecto: si esta imagen abarcara todo el ancho de una página Carta (Letter), ¿cuál sería su resolución? Solo se tocan las imágenes cuya estimación supera MaxDPI. El sesgo es intencional: una imagen colocada en tamaño pequeño en la página tiene un PPP real superior a la estimación, por lo que la pasada se activa de menos en lugar de degradar un recurso con calidad de impresión que no puede medir

Quality de 1 a 100 selecciona la calidad de codificación de nuevo de JPEG, mientras que 0 mantiene la salida como Flate sin pérdidas de estilo PNG; Filter elige el núcleo de remuestreo, 0 para un promedio de caja (box average) y 1 para bilineal. Para trámites de oficina escaneados, DownsampleImages(150, 75, 1) es un punto de partida razonable; para cualquier cosa que se pueda volver a imprimir, aumente MaxDPI a 300 o descarte la pasada por completo. El remuestreo a la baja es el único paso con pérdidas de los tres, por lo que debe estar detrás de una opción que sus usuarios puedan desactivar

Convertir flujos LZW heredados con NormalizeLZWStreams

NormalizeLZWStreams es una victoria gratuita: descomprime sin pérdidas cada flujo LZWDecode y lo vuelve a comprimir con FlateDecode, en su lugar, devolviendo el recuento de flujos convertidos. Maneja tanto una sola entrada /Filter /LZWDecode como LZW que aparece dentro de una matriz de cadena de filtros, donde solo se reemplaza el enlace LZW y se conserva el resto de la cadena. Los parámetros del predictor (Predictor, Columns, Colors, BitsPerComponent) se leen del DecodeParms del flujo y se pasan al descompresor, de modo que los datos de imagen codificados con predictor realizan el viaje de ida y vuelta correctamente. Debido a que ambos filtros son códecs exactos a nivel de bits, los bytes decodificados son idénticos antes y después; solo cambia la compresión del contenedor, razón por la cual esta pasada es segura de ejecutar incondicionalmente en cada archivo

En un documento sin flujos LZW, la llamada simplemente devuelve 0 y no toca nada, lo que el conjunto de pruebas de regresión de la biblioteca ejercita explícitamente: un archivo solo Flate recién creado debe informar cero conversiones. Esa garantía de no hacer nada es importante cuando la pasada se encuentra en una canalización que procesa miles de archivos heterogéneos, algunos de 2024 y algunos de 1998

La canalización completa de optimización de tamaño en Delphi

Las tres pasadas se combinan en una única función de cargar-optimizar-guardar, y el orden importa menos de lo que cabría esperar porque operan en tipos de objetos disjuntos: fuentes, XObjects de imagen y filtros de flujo. Ejecutar primero la subdivisión sigue siendo la opción ordenada, ya que es la pasada con una restricción de orden de edición

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 la canalización de la forma en que la biblioteca se verifica a sí misma: viaje de ida y vuelta. Las pruebas de regresión de la versión 3.130 crean un documento, lo guardan, lo vuelven a cargar, ejecutan la optimización, lo guardan de nuevo y luego afirman tres cosas: la salida es más pequeña, los recuentos devueltos coinciden con las expectativas y una nueva carga del archivo optimizado todavía se analiza y representa. Reproducir ese bucle de crear-optimizar-recargar con una muestra de sus propios archivos de producción y comparar el texto extraído antes y después es una inversión de una hora que detecta errores de integración mucho antes de que un cliente abra una factura dañada

// 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;

¿Dónde encaja la canalización en un flujo de trabajo de fusión? Después de la fusión, no durante ella. Fusionar primero y optimizar el resultado único significa que cada fuente incrustada se subdivide una vez frente a la unión de todos los caracteres utilizados, en lugar de por archivo de origen. Si el rendimiento de la fusión es el cuello de botella, PDFlibPas ofrece una ruta rápida a nivel de bytes que evita el análisis completo de objetos, descrita en el artículo sobre la fusión rápida de PDF con desplazamiento de referencias de bytes; y para entradas demasiado grandes para guardarse completamente en la memoria, la fusión y división con acceso directo para PDF grandes cubre la ruta de transmisión. Ambas se combinan de forma natural con una pasada de optimización final en la salida fusionada

Lo que las tres pasadas no harán

El trío de optimización de losLab PDF Library excluye deliberadamente cualquier cosa que cambie la semántica del documento. SubsetEmbeddedFonts no unifica fuentes duplicadas de orígenes fusionados en un solo programa, reduce cada una de forma independiente; la deduplicación es una transformación diferente y más riesgosa. DownsampleImages pasará por alto una imagen cuya estimación de PPP conservadora se mantenga por debajo del umbral, incluso cuando un humano pueda notar que está sobredimensionada para su marco. Y ninguna de las pasadas toca la estructura del documento, por lo que un archivo saturado por miles de objetos huérfanos necesita un guardado de estilo reescritura en lugar de estas pasadas a nivel de flujo. Dentro de esos límites, la combinación de subdivisión de fuentes, muestreo a la baja de imágenes y normalización de LZW a Flate elimina las tres fuentes clásicas de exceso de tamaño de PDF con una sola llamada a la API predecible para cada una. Las tres funciones se distribuyen como parte de losLab PDF Library para Delphi, C# y VB.NET, junto con las API de fusión, extracción y representación analizadas anteriormente