Para averiguar a dónde va realmente el tamaño de un archivo PDF, losLab PDF Library expone AuditDocumentSpace, que clasifica cada objeto indirecto en doce categorías —imágenes, programas de fuente, diccionarios de fuente, flujos de contenido, XObjects de formulario, flujos de objetos, archivos incrustados, metadatos, árbol de estructura, anotaciones, árbol de páginas, otros— e informa del número de objetos, los bytes almacenados y el porcentaje de participación de cada una
La situación para la que existe esto es familiar. Un informe de 40 páginas sale de tu generador con 80 MB, el cliente pregunta por qué, y lo único que puedes ofrecer es una suposición. Probablemente las imágenes. Quizá las fuentes. Así que activas el submuestreo, lo publicas, y el archivo se queda en 74 MB porque el peso real estaba en otro sitio completamente distinto. Nuestro artículo complementario sobre subconjunto de fuentes y submuestreo de imágenes cubre cómo reducir un PDF; este cubre el paso que debería venir antes, que es medir lo que estás a punto de reducir
¿Por qué medir antes de comprimir?
Porque las tres pasadas de optimización estándar tienen resultados muy distintos según el archivo, y nada en el archivo te dice cuál aplica hasta que cuentas. Aplicar subconjunto de fuentes a un documento cuyas fuentes ya son el 2% de sus bytes es una tarde dedicada a mover un error de redondeo. Submuestrear imágenes en un archivo cuyo grueso son flujos de contenido sin comprimir produce la misma decepción. El optimizador no es la parte difícil —toda librería tiene uno—. Saber a qué optimizador apuntar en este archivo es la parte difícil, y eso es una cuestión contable, no una cuestión de compresión. Una auditoría también detecta los casos en los que ningún optimizador es la respuesta: un archivo que resulta ser un 60% de adjuntos incrustados no necesita mejor compresión, necesita una conversación sobre si esos adjuntos deben estar en el documento, y un archivo que es un 30% árbol de estructura está pagando por el etiquetado de accesibilidad, que suele ser un coste deliberado que no deberías eliminar en silencio. Una vez atribuidos los bytes, tomas una decisión de producto con cifras detrás en lugar de recurrir al interruptor más a mano
Qué contiene el informe de doce categorías
AuditDocumentSpace devuelve un manejador de lista de cadenas en lugar de un registro, de modo que el informe sobrevive intacto a las fachadas planas de DLL y COM. La lista contiene una línea de resumen Total,Objects,Bytes,100.0 seguida de exactamente doce líneas Category,Objects,Bytes,Percent en un orden fijo que forma parte del contrato: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Trece líneas, siempre, incluso cuando una categoría está vacía
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
Un detalle de Delphi en ese bucle te morderá exactamente una vez. GetStringListItem usa índices de elemento en base uno, a juego con GetStringListCount, y un índice fuera de rango devuelve una cadena vacía en lugar de lanzar una excepción. Escribe el bucle como for I := 0 to Count - 1 por costumbre y obtendrás una primera línea en blanco, una última línea descartada en silencio, y ninguna excepción en ningún sitio que te avise de que la indexación está mal. El propio informe se verá casi correcto, que es el peor modo de fallo que puede tener una herramienta de diagnóstico
¿Por qué la auditoría usa la longitud almacenada en lugar del tamaño decodificado?
Porque la longitud almacenada es a la vez el número que quieres y el número que es barato de obtener. Cada objeto indirecto lleva TPDFIndObj.FLength, la longitud en bytes en bruto que ocupa el objeto en el archivo tal y como se analizó. Usarla significa que una imagen DCTDecode de 900 KB se informa como 900 KB —los bytes que te cuesta en disco— en lugar de los 40 MB de muestras RGB a los que decodifica. También significa que la auditoría nunca tiene que decodificar nada: los objetos cargados de forma perezosa siguen siendo perezosos, los filtros siguen sin ejecutarse, y auditar un archivo de 500 MB es una pasada sobre cabeceras de objeto en lugar de un ciclo completo de descompresión
La segunda regla es una defensa contra el doble conteo. Cuando un objeto vive dentro de un flujo de objetos comprimido, indicado por un FObjStrNum distinto de cero, su recuento de bytes se registra como cero. Su almacenamiento ya se ha pagado una vez a través del flujo contenedor, que la norma ISO 32000-1 §7.5.7 define como un flujo /Type /ObjStm que contiene muchos objetos en una única carga comprimida con Flate. Cobrarle a cada miembro su propia parte y luego volver a cobrar el contenedor inflaría el total por encima del tamaño real del archivo. Esto tiene una consecuencia directa en cómo se lee la salida, tratada más abajo y con más profundidad en nuestro artículo sobre flujos de objetos y flujos de referencias cruzadas
¿Por qué un programa de fuente no puede clasificarse a sí mismo?
Porque un archivo de fuente TrueType incrustado en un PDF no tiene ningún marcador que lo indique. La norma ISO 32000-1 §9.8.1 define el programa de fuente incrustado como el valor de /FontFile, /FontFile2 o /FontFile3 en un descriptor de fuente, y el diccionario de flujo en el otro extremo de esa referencia lleva /Length1 y claves de filtro pero ningún /Type ni ningún /Subtype que lo identifique como una fuente. Visto de forma aislada es un flujo binario anónimo. Solo el descriptor que apunta a él sabe qué es. La misma asimetría aparece en las anotaciones: el §12.5.2 hace que /Type /Annot sea opcional en un diccionario de anotación, así que la señal fiable es la pertenencia a un array /Annots de página, no el propio diccionario
Así que la clasificación se ejecuta dos veces. La primera pasada lee el propio /Type y /Subtype de cada objeto y se lleva las victorias fáciles: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font y /Type /FontDescriptor, /Metadata, /EmbeddedFile y /Filespec, /StructTreeRoot y /StructElem, /Annot, /Page y /Pages. Todo lo demás aterriza provisionalmente en Other. La segunda pasada recorre entonces el lado que hace referencia y reasigna: cada diccionario de página reasigna su /Contents a flujos de contenido, sus entradas /Annots a anotaciones, y su /Thumb a imágenes, mientras que cada diccionario de fuente recorre su propia cadena de descriptor
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
Leer el informe y elegir el siguiente movimiento
Lee primero los porcentajes, luego los recuentos de objetos, y trata cualquier brecha grande entre ambos como una señal. Un PDF moderno mete la mayoría de sus diccionarios pequeños dentro de flujos de objetos, así que Page tree y Structure tree muestran habitualmente docenas de objetos frente a casi cero bytes —su coste real se ha plegado en la línea Object streams—. Si Object streams es en sí mismo grande, el archivo está denso de estructura similar a metadatos en lugar de contenido, y la palanca es podar objetos, no comprimirlos. Los flujos de apariencia de anotación se comportan de forma similar: llevan /Subtype /Form, así que un documento muy sellado muestra su peso bajo Form XObjects mientras la línea Annotations se mantiene pequeña
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
Dos datos de formato importan si analizas los porcentajes en lugar de simplemente mostrarlos. El separador decimal es siempre un punto literal con independencia de la configuración regional de la máquina, así que analizarlo con el FormatSettings ambiente en un puesto de trabajo alemán o francés fallará o, peor, lo interpretará mal. Y los ceros finales se recortan, así que una categoría que contenga exactamente el 40% de los bytes se imprime como 40, no como 40.0 —nunca asumas un número fijo de decimales—. Con el porcentaje en mano, el enrutamiento es mecánico: una participación dominante de Images apunta a DownsampleImages, una participación dominante de Font programs a SubsetEmbeddedFonts, y unos Content streams voluminosos a CompressContent
Lo que la auditoría deliberadamente no te dice
El total es una suma sobre objetos indirectos, y un archivo PDF es algo más que sus objetos. La cabecera del archivo, el trailer, los espacios en blanco entre objetos y una tabla de referencias cruzadas clásica no son objetos indirectos, así que esos bytes no se atribuyen a nada y el total de la auditoría queda un poco por debajo del tamaño en disco. Un flujo de referencias cruzadas es distinto —es un objeto real con /Type /XRef—, así que en un archivo moderno esos bytes sí aparecen, en la categoría Other. Ninguno de los dos comportamientos es un defecto, pero si estás conciliando la auditoría con un recuento de bytes del sistema de archivos, de ahí viene la diferencia
Vale la pena señalar con claridad dos límites más. Primero, las cifras describen un archivo que se cargó, no uno que se está redactando: para los objetos construidos en memoria que todavía no tienen longitud almacenada, el tamaño recurre a la salida serializada con una asignación nominal para el diccionario de flujo, que es una estimación de la escritura eventual y no una medición. Audita después de guardar y recargar si quieres cifras exactas. Segundo, una línea Other abultada es un hallazgo, no un informe de fallo —normalmente significa objetos huérfanos a los que ya no hace referencia nada, que es tarea para la recolección de basura mark-and-sweep y no para ninguna pasada de compresión
Usada así, la auditoría cambia la forma de la conversación. En lugar de adivinar sobre el informe de 80 MB, lo abres, ejecutas una llamada, y lees que las imágenes son el 8%, los programas de fuente el 61%, y que el documento incrusta nueve programas de fuente completos para un estilo corporativo que usa tres tipografías. Esa es una respuesta solucionable con un número asociado. AuditDocumentSpace, junto con las pasadas de optimización a las que apunta, se incluye en la losLab PDF Library para Delphi y C++Builder, donde las páginas de referencia documentan la lista completa de categorías y la API de lista de cadenas que la rodea