Artículo técnico

HotPDF CompressDocument: subconjuntos de fuentes compactos

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ó

Diagrama del ciclo de vida de CompressDocument de HotPDF en Delphi: BeginDoc anota los valores propios del writer, sobrescribe seis ajustes incluidos Compression y UseObjectStreams para un documento, y EndDoc restaura cada valor prestado en su finally más externo mientras la propiedad CompressDocument en sí se queda en True
Seis ajustes del writer y el tope de object streams se toman prestados para exactamente un documento y se devuelven cuando corre EndDoc, así que un informe fallido nunca deja el componente atascado en compresión máxima
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

Comparación de un subset de fuentes disperso de HotPDF, que conserva entradas loca y hmtx para cada glyph ID original hasta el GID retenido más alto, con el output de CompactFontSubsetting, que renumera los glifos conservados densamente desde cero y mapea los CIDs por un stream CIDToGIDMap mientras content streams, /W y ToUnicode quedan sin cambios
Renumera saca el coste del programa de la fuente y lo mete en un pequeño stream de mapa — dos caracteres SimSun bajaron de 24.8 KB a 3.1 KB sin tocar un byte del contenido ya escrito

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 /Page se 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

Flujo de CompressLoadedDocument de HotPDF en Delphi: la llamada elimina recursos de página sin usar, fusiona fuentes y formularios idénticos, hace subsetting de las fuentes incrustadas con subconjuntos compactos, recomprime streams con Flate solo cuando el resultado es más pequeño, y activa los object streams para el siguiente guardado, mientras que los campos de firma disparan RefusedBySignaturePolicy y dejan el archivo intacto
Cada paso reescribe bytes que cubre una firma, así que el documento entero se rechaza salvo que permita explícitamente la invalidación — Info.BytesSaved suma entonces solo el trabajo de recursos, fuentes y streams
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