HotPDF THotPDF.CompressDocument es un único interruptor que hace que BeginDoc produzca el PDF sin pérdidas más pequeño que el componente sabe escribir: FlateDecode al nivel máximo, una cross-reference stream con object streams, font subsetting y subconjuntos de fuentes compactos que renumeran los glifos conservados detrás de un /CIDToGIDMap explícito. EndDoc devuelve después sus propios ajustes. Un documento de prueba de tres páginas con Arial y SimSun bajó de 10.2 MB a 20 KB con renderizado idéntico
¿Qué activa realmente CompressDocument?
CompressDocument sobrescribe seis ajustes del writer, más el tope de object streams, durante un documento y los restaura todos después. En BeginDoc, antes de que la versión del PDF se asiente, HotPDF anota sus valores y pone Compression en cmFlateDecode, CompressionLevel en clMaximum, activa EnableFontSubsetting y CompactFontSubsetting, y habilita UseXRefStream junto a UseObjectStreams (ISO 32000-1 §7.5.7 y §7.5.8). Los object streams necesitan PDF 1.5, así que un Version anterior sube a 1.5 cuando no está bloqueado. PDF/A-1 prohíbe ambas estructuras, así que un documento PDF/A-1 conserva su tabla de referencias cruzadas clásica y solo recibe el trabajo de Flate y de fuentes. Las imágenes se dejan tal cual las incrustó
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // lo aplica BeginDoc, lo deshace EndDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
La restauración ocurre en el finally más externo de EndDoc, así que una excepción a mitad de un informe no deja un componente de larga vida atascado en compresión máxima para el siguiente trabajo. La propiedad CompressDocument en sí se queda en True; solo los seis ajustes que tomó prestados vuelven atrás. La versión se maneja con más cuidado. HotPDF deshace su propia subida a 1.5 solo si el documento sigue acabando en 1.5, así que cuando otra feature empujó el archivo a 1.6 durante la ejecución (una fuente OpenType incrustada, por ejemplo), la versión mayor se queda, exactamente como habría quedado sin compresión
¿Por qué los subconjuntos de fuentes siguen siendo grandes sin compactación?
Un subconjunto TrueType clásico suelta los contornos que nunca dibuja pero conserva cada glyph ID donde estaba, y esa numeración es lo que lo mantiene pesado. El content stream muestra CIDs que igualan los GID originales, así que el subconjunto tiene que conservar un offset de loca y una entrada de hmtx para cada hueco hasta el glifo más alto que retiene, vacío o no. Para una fuente latina ese sobrecoste es ruido. Para una fuente CJK como SimSun, cuyos ideogramas viven en lo hondo de una tabla de glifos enorme, dos caracteres chinos arrastran tablas dimensionadas para la fuente entera. Las reglas de cierre del subset de fuentes para glifos con shaping deciden qué glifos sobreviven; la compactación va de cuánto cuestan los supervivientes
CompactFontSubsetting renumera los glifos conservados en un rango denso que empieza en cero y escribe un stream de /CIDToGIDMap en la CIDFont, que ISO 32000-1 §9.7.4.2 define como una tabla de GID de dos bytes indexada por CID. Esa tabla es todo el truco. Los content streams, el array de anchos /W y el CMap ToUnicode conservan los CIDs originales, así que nada de lo ya escrito tiene que cambiar; solo el paso de CID a glifo se muda al mapa. En el test que motivó la feature, SimSun con dos caracteres pasó de 24.8 KB de datos de fuente a 3.1 KB
La compactación tiene límites firmes, y se degrada en silencio en lugar de fallar. HotPDF construye subconjuntos compactos solo para fuentes Type 0 TrueType, tanto las puestas con SetFont con subsetting activado como la fuente registrada por RegisterUnicodeTTF. Una fuente TrueType simple encuentra sus glifos por el cmap dentro del programa de la fuente, que la renumeración rompería, así que conserva el subconjunto disperso. Las fuentes OpenType-CFF tampoco tienen vía compacta. Una construcción compacta que falla recurre al subconjunto disperso en lugar de lanzar. La propiedad viene apagada por defecto, así que el output existente se mantiene idéntico byte a byte, mientras que bajo PDF/A la fuente Unicode registrada siempre recibe un subconjunto compacto
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // usable sin CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
¿Cómo aprieta el writer empaquetado la estructura del archivo?
Cuando las fuentes y los streams ya son pequeños, los diccionarios y los datos de referencias cruzadas se convierten en el mayor coste restante, así que el writer de object streams detrás de CompressDocument recorta también eso. La guía de object streams y actualizaciones incrementales cubre el formato del contenedor en sí; la vía de compresión añade cuatro refinamientos encima:
- Sintaxis compacta según ISO 32000-1 §7.2.2: un espacio solo se escribe entre dos tokens que de otro modo se pegarían como caracteres regulares, así que
/Type /Pagese convierte en/Type/Page - Los campos del cross-reference stream toman cualquier anchura que §7.5.8.2 permita, así que un archivo de menos de 16 MB guarda cada offset en 3 bytes en lugar de 4
- Hasta 250 objetos entran en cada object stream en lugar de los 100 habituales, salvo que fije su propio tope con
ConfigureAdaptiveObjectStreamPacking - Cuando el archivo no está cifrado, el Catalog y el diccionario Info también se empaquetan en object streams; el output cifrado los deja en el nivel superior
La sintaxis compacta vino con una trampa que conviene conocer si extiende el writer. La firma rellena la signature después de escribir el archivo buscando en los bytes los placeholders literales /ByteRange ( y /Contents <, y la escritura compacta los convertiría en /ByteRange( y /Contents<, que la búsqueda jamás encuentra. Los diccionarios de firma (Type Sig o DocTimeStamp, FT Sig) y el diccionario de cifrado conservan por eso el diseño con espacios. Un defecto relacionado afectó a builds anteriores a v2.766.41: cada guardado con object streams, CompressDocument incluido, empezaba con dos líneas de cabecera %PDF-, así que actualice si un validador estricto marca su output
¿Se puede comprimir un PDF que ya está cargado?
Sí, por la sobrecarga con opciones CompressLoadedDocument(Options, Info), que ejecuta los mismos pasos sin pérdidas sobre un archivo existente. Con THPDFLoadedDocumentCompressionOptions.Default elimina recursos de página sin usar, fusiona fuentes y formularios idénticos, hace subsetting de las fuentes incrustadas con subconjuntos compactos activados, recomprime streams sin filtrar, Flate, LZW, ASCII y RunLength con Flate cuando el resultado es más pequeño, y hace que el siguiente guardado use object streams. HighRatioFlate viene apagado por defecto, y los object streams se saltan para PDF/A-1 y guardados incrementales. La sobrecarga sin parámetros CompressLoadedDocument es la llamada antigua y más estrecha que solo comprime con Flate los streams sin comprimir
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
Dos fronteras importan en la vía de documento cargado. Cada paso reescribe bytes que cubre una firma, así que un documento con campos de firma se rechaza como un todo: la llamada devuelve 0, pone RefusedBySignaturePolicy y no cambia nada, salvo que active AllowSignatureInvalidation, tras lo cual Info.SignaturesInvalidated le cuenta a qué renunció. La compactación también es más conservadora aquí que en la vía de creación. HotPDF compacta solo programas de fuente usados exclusivamente por fuentes CIDFontType2 con un /CIDToGIDMap Identity, donde CID iguala GID, y se salta programas con un stream de mapa existente, un /CIDSet o tablas de glifos de color como COLR, sbix, CBDT o SVG, porque la reconstrucción compacta soltaría las capas de color. Note también que Info.BytesSaved suma solo los pasos de recursos, fuentes y streams; la ganancia de object streams aparece cuando se escribe el archivo
¿Qué resultados cabe esperar en la práctica?
Las ganancias siguen cuánto del archivo es estructura sin comprimir y datos de fuente sobredimensionados, no cuántas páginas tiene. La muestra de tres páginas con Arial y SimSun se encogió de 10.2 MB a 20 KB generada con CompressDocument, y de 10.2 MB a 19.8 KB cuando el original sin comprimir se cargó y pasó por CompressLoadedDocument, con renderizado idéntico por ambas vías. Un PDF que ya es compacto casi no se mueve: en el conjunto de regresión, tales archivos se salvaron dentro de un -0.07% a +0.06% de su tamaño original. Los archivos con muchas fotos ganan poco, porque ninguna de las dos vías toca los datos de imagen
Si genera los mismos informes CJK cada noche, combine los subconjuntos compactos con la caché de subconjuntos de fuentes persistente en disco para no repetir el trabajo de subsetting en cada ejecución, y compare por diff los outputs comprimidos por contenido de objeto en lugar de por bytes, ya que un solo campo cambiado vuelve a pasar por Flate un object stream entero. Las referencias completas de propiedades y records están en la página de producto del componente PDF HotPDF para Delphi