THotPDF.CompressDocument de HotPDF es un switch único que hace que BeginDoc produzca el PDF sin pérdida más chico que el componente puede escribir: FlateDecode al nivel máximo, un cross-reference stream con object streams, font subsetting y subsets de fuentes compactos que renumeran los glyphs conservados detrás de un /CIDToGIDMap explícito. EndDoc después devuelve sus propios settings al lugar. 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 en realidad CompressDocument?
CompressDocument sobrescribe seis settings del escritor, más el tope de object streams, para un documento y los restaura todos después. En BeginDoc, antes de que la versión del PDF quede decidida, HotPDF anota sus valores y pone Compression en cmFlateDecode, CompressionLevel en clMaximum, enciende EnableFontSubsetting y CompactFontSubsetting, y habilita UseXRefStream junto con UseObjectStreams (ISO 32000-1 §7.5.7 y §7.5.8). Los object streams necesitan PDF 1.5, así que una Version más vieja se sube a 1.5 cuando no está bloqueada. PDF/A-1 prohíbe ambas estructuras, así que un documento PDF/A-1 conserva su tabla de cross-reference clásica y solo recibe el trabajo de Flate y de fuentes. Las imágenes se dejan exactamente como usted las embebió
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 sucede en el finally más externo de EndDoc, así que una excepción a mitad de un reporte no deja un componente de larga vida atascado en compresión máxima para el siguiente job. La propiedad CompressDocument en sí se queda en True; solo los seis settings que pidió prestados regresan. La versión se maneja con más cuidado. HotPDF deshace su propia subida a 1.5 solo si el documento sigue terminando en 1.5, así que cuando otra feature empujó el archivo a 1.6 durante la corrida (una fuente OpenType embebida, digamos), la versión más alta se queda, exactamente como habría quedado sin compresión
¿Por qué los subsets de fuentes siguen siendo grandes sin compactación?
Un subset TrueType clásico suelta los outlines 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 GIDs originales, así que el subset tiene que conservar un offset de loca y una entrada de hmtx por cada slot hasta el glyph más alto que retiene, vacío o no. Para una fuente latina ese overhead es ruido. Para una fuente CJK como SimSun, cuyos ideographs viven hondo en una tabla de glyphs enorme, dos caracteres chinos arrastran tablas dimensionadas para la fuente completa. Las reglas de cierre de subsets de fuentes para glyphs con shaping deciden qué glyphs sobreviven; la compactación trata de cuánto cuestan los sobrevivientes
CompactFontSubsetting renumera los glyphs conservados en un rango denso que arranca en cero y escribe un stream de /CIDToGIDMap sobre el CIDFont, que ISO 32000-1 §9.7.4.2 define como una tabla de GIDs de dos bytes indexada por CID. Esa tabla es todo el truco. Los content streams, el array de widths /W y la CMap de ToUnicode conservan los CIDs originales, así que nada de lo ya escrito tiene que cambiar; solo el lookup de CID a glyph se muda al mapa. En el test que motivó la feature, SimSun con dos caracteres pasó de 24.8 KB de data de fuente a 3.1 KB
La compactación tiene límites firmes, y se degrada en silencio en vez de fallar. HotPDF arma subsets compactos solo para caras Type 0 TrueType, tanto las que se fijan vía SetFont con subsetting encendido como la cara registrada vía RegisterUnicodeTTF. Una fuente TrueType simple encuentra sus glyphs por el cmap dentro del programa de la fuente, cosa que la renumeración rompería, así que conserva el subset disperso. Las caras OpenType-CFF tampoco tienen camino compacto. Un build compacto que falla cae de vuelta al subset disperso en lugar de lanzar excepción. La propiedad viene apagada de fábrica, así que la salida existente queda idéntica byte a byte, mientras que bajo PDF/A la cara Unicode registrada siempre recibe un subset 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 escritor packed la estructura del archivo?
Cuando las fuentes y los streams ya son chicos, los diccionarios y la data de cross-reference pasan a ser el mayor costo restante, así que el escritor de object streams detrás de CompressDocument también los recorta. La guía de object streams y actualizaciones incrementales cubre el formato del contenedor; el camino de compresión suma cuatro refinamientos encima:
- Sintaxis compacta según ISO 32000-1 §7.2.2: un espacio se escribe solo entre dos tokens que de otro modo quedarían pegados como caracteres regulares, así que
/Type /Pagese vuelve/Type/Page - Los campos del cross-reference stream toman cualquier ancho 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 de siempre, salvo que fije su propio tope vía
ConfigureAdaptiveObjectStreamPacking - Cuando el archivo no está encriptado, el Catalog y el diccionario Info también se empaquetan en object streams; la salida encriptada los deja en el nivel superior
La sintaxis compacta vino con una trampa que vale conocer si extiende el escritor. La firma rellena la signature después de escrito el archivo buscando en los bytes los placeholders literales /ByteRange ( y /Contents <, y la grafía 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 encriptación conservan por eso el layout con espacios. Un defecto relacionado afectó a builds anteriores a la v2.766.41: cada guardado con object streams, CompressDocument incluido, arrancaba con dos líneas de header %PDF-, así que actualice si un validator estricto marca su salida
¿Se puede comprimir un PDF ya cargado?
Sí, vía la sobrecarga con opciones CompressLoadedDocument(Options, Info), que corre los mismos pasos sin pérdida sobre un archivo existente. Con THPDFLoadedDocumentCompressionOptions.Default remueve recursos de página sin uso, fusiona fuentes y forms idénticos, hace subsetting de las fuentes embebidas con subsets compactos activados, recomprime streams sin filtrar, Flate, LZW, ASCII y RunLength con Flate cuando el resultado es más chico, y hace que el próximo 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 de CompressLoadedDocument es la llamada vieja y más angosta 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 el camino de documentos cargados. Cada paso reescribe bytes que una firma cubre, 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 usted fije AllowSignatureInvalidation, tras lo cual Info.SignaturesInvalidated le dice a qué renunció. La compactación también es más conservadora aquí que en el camino de creación. HotPDF compacta solo programas de fuente usados únicamente por fuentes CIDFontType2 con /CIDToGIDMap Identity, donde CID iguala a GID, y se salta los programas con un stream de mapa existente, un /CIDSet, o tablas de glyphs de color como COLR, sbix, CBDT o SVG, porque la reconstrucción compacta soltaría las capas de color. Note además que Info.BytesSaved suma solo los pasos de recursos, fuentes y streams; la ganancia de object streams aparece cuando el archivo se escribe
¿Qué resultados cabe esperar en la práctica?
Las ganancias siguen cuánto del archivo es estructura sin comprimir y data de fuente sobredimensionada, no cuántas páginas tiene. La muestra de tres páginas con Arial y SimSun se achicó de 10.2 MB a 20 KB al generarse con CompressDocument, y de 10.2 MB a 19.8 KB cuando el original sin comprimir se cargó y se pasó por CompressLoadedDocument, con renderizado idéntico de ambos modos. Un PDF que ya es compacto casi no se mueve: en el set de regresión, esos archivos se guardaron dentro de -0.07% a +0.06% de su tamaño original. Los archivos cargados de fotos ganan poco, porque ninguno de los dos caminos toca la data de imágenes
Si genera los mismos reportes CJK cada noche, combine los subsets compactos con la caché de subsets de fuentes persistente en disco para no repetir el trabajo de subsetting en cada corrida, y haga diff de las salidas comprimidas por contenido de objeto en vez de por bytes, porque un campo cambiado re-Flatea un object stream entero. Las referencias completas de propiedades y records están en la página de producto del componente HotPDF Delphi PDF