Genere un informe, incruste una fuente TrueType y la salida se abrirá correctamente en cualquier visor que pruebe. Los glifos son correctos, el texto se puede seleccionar y el archivo es válido. Lo único que falla es el tamaño. Un documento que utiliza unas pocas docenas de caracteres latinos transporta la fuente completa de 350 KB. Un documento que imprime un párrafo en chino transporta una fuente CJK de 14 MB en lugar de la porción de medio megabyte que debería necesitar. No se produjo ninguna excepción, no se registró ninguna advertencia y el archivo pasó la validación. Así es como se ve desde fuera un paso de finalización mal ordenado: nada falla y la única evidencia es un tamaño de archivo excesivo
Merece la pena escribirlo como una lección, porque la forma del error es general. Cualquier motor de documentos tiene una etapa de finalización que muta los objetos justo antes de escribirlos, y la corrección de esa etapa depende completamente del orden de sus pasos con respecto a la serialización. Coloque un paso en el lado equivocado de la escritura y no hará nada, silenciosamente
Lo que se supone que debe hacer el subconjunto de fuentes
Una fuente de subconjunto es la parte de un archivo TrueType que un documento realmente utiliza. La norma ISO 32000-1 §9.9 describe cómo un programa de fuente incrustado viaja en un flujo referenciado por el descriptor de fuente, y para un programa TrueType ese flujo es /FontFile2 con un /Length1 que indica el recuento de bytes sin comprimir. El subconjunto reescribe las tablas glyf y loca para que contengan solo los glifos a los que hace referencia el documento, renombra los identificadores de glifo y antepone al nombre de /BaseFont una etiqueta de seis letras como ABCDEF+ para marcar la fuente como un subconjunto, tal como lo requiere la especificación. Una tipografía latina que se reduce a diez o quince kilobytes representa la diferencia entre un PDF ligero y uno que incluye una tipografía completa solo por un encabezado
El momento en que esto ocurre importa. El subsetting no es una transformación que se aplique a bytes que ya están en disco. Edita el grafo de objetos en memoria: encoge el contenido del flujo /FontFile2, corrige /Length1 y reescribe la cadena /BaseFont. Todo eso tiene que estar en su sitio cuando el serializador recorre el grafo y emite bytes. Si las ediciones aterrizan después de que los bytes ya se han escrito, actualizan objetos que nadie va a leer jamás
El síntoma y por qué nada se quejó
Cuando el subconjunto aterriza en el lado equivocado de la escritura, el síntoma son fuentes completas en la salida sin ningún diagnóstico. Registre una fuente TrueType Unicode, produzca un documento normal y el objeto de fuente incrustado saldrá con la misma longitud que el archivo .ttf de origen, y el nombre de /BaseFont sin el prefijo de subconjunto de seis letras. La salida nunca se reduce entre ejecuciones que usan diez glifos y ejecuciones que usan diez mil
La ausencia de cualquier error es lo que hace que esta clase de fallos sea costosa. Una rutina de creación de subconjuntos que se ejecuta en el momento equivocado se sigue ejecutando. Recorre el uso acumulado de puntos de código, crea un subconjunto perfectamente correcto y lo aplica al gráfico de objetos en memoria. Internamente, el trabajo se realiza y la llamada regresa limpiamente. Lo único incorrecto es que el gráfico de objetos que editó ya no es el que se está escribiendo, porque el escritor ya terminó. Desde el punto de vista de la llamada, el documento se generó y guardó sin incidentes, que es precisamente la impresión que da un fallo silencioso
Dónde encaja el subconjunto en el orden de finalización
En HotPDF, el trabajo de cierre ocurre dentro de EndDoc. El paso de subconjunto es una rutina interna llamada BuildAndApplyUnicodeFontSubset. Lee el conjunto de puntos de código usados por documento, mantenido en un mapa de bits que la ruta de emisión de texto llena a medida que se muestran los glifos, mapea cada punto de código usado a través de la tabla de puntos de código a glifo en caché a un identificador de glifo real y reescribe el programa de fuente alrededor de ese cierre. Cuando se registra una fuente TrueType Unicode, la ruta de emisión establece un bit en el conjunto de puntos de código usados para cada carácter que dibuja, por lo que para cuando el documento se cierra, el motor sabe exactamente qué glifos debe conservar el subconjunto
La trampa está en la posición de esa llamada. Si BuildAndApplyUnicodeFontSubset se invoca después de que SaveToStream o SaveToFile ya hayan serializado el documento, las ediciones del creador de subconjuntos en /FontFile2, su /Length1 corregido y el prefijo de seis letras de /BaseFont se calculan todos sobre un gráfico de objetos que ya se ha convertido en bytes. La secuencia correcta ejecuta primero el creador de subconjuntos y serializa después, de modo que el escritor emite la fuente en subconjunto en lugar de la original. Hacerlo bien es cuestión de la posición de una llamada, nada más
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // el subsetting corre aquí, antes de escribir
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Nada de esto es visible en el código de llamada. El subconjunto está activado por defecto una vez que se ha registrado una fuente TrueType Unicode. Registra la fuente, inicia el documento, dibuja y lo finaliza, y el subconjunto se crea a partir de los glifos que utilizó antes de que los bytes salgan de la memoria
Por qué un paso mal colocado representa una categoría completa
La razón por la que esto merece una lección en lugar de una nota a pie de página es que EndDoc emite una lista de pasos de cierre, y cada uno de ellos es sensible a su posición relativa a la escritura. El subconjunto de fuentes es uno de ellos. La salida PDF/A requiere un flujo /CIDSet que enumere exactamente los identificadores de glifo presentes en el subconjunto, una restricción que impone la norma ISO 19005 para que un validador pueda confirmar que el programa incrustado coincide con lo que afirma el descriptor de fuente; ese flujo se emite en la misma ventana de finalización y depende de que el subconjunto se haya creado primero. La norma PDF/UA-1 requiere, según ISO 14289-1 §7.18.3, que cada página que lleve una anotación declare /Tabs con el valor /S, y una rutina interna llamada EnsurePDFUATabsOnAnnotatedPages estampa esa clave durante la misma etapa. Las comprobaciones de intención de salida también se ejecutan allí
El mismo fallo de ordenación que deja el subconjunto sin efecto también omitiría la clave de orden de tabulación PDF/UA en las páginas anotadas, porque ese paso se encuentra en la misma ventana. veraPDF y PAC reportan la falta de /Tabs /S como una violación del punto de control 21-001 del protocolo Matterhorn. De modo que una sola llamada mal colocada no solo infla el tamaño del archivo; rompe silenciosamente un requisito de conformidad de accesibilidad al mismo tiempo, con la misma ausencia de errores. Ese es el riesgo de una etapa de finalización: sus pasos comparten una condición previa, y un solo error de ordenación puede anular varios de ellos a la vez mientras cada llamada sigue devolviendo éxito
Cómo se detecta realmente un fallo de emisión silencioso
Un error que no produce excepciones no se detecta ejecutando el programa. Se detecta inspeccionando la salida y comparándola con lo que debería haber producido la entrada. Para el subconjunto de fuentes las comprobaciones son concretas. Compare el tamaño del archivo de salida con una expectativa aproximada: un documento que utilizó un puñado de glifos no debería tener el tamaño de una tipografía completa. Abra el objeto de fuente incrustado y lea su longitud en bytes; un /FontFile2 con subconjunto para una tipografía latina es una pequeña fracción del archivo de origen. Lea el nombre de /BaseFont y confirme que el prefijo de seis letras esté presente, porque su ausencia es una señal directa de que no se aplicó ningún subconjunto
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// Unos pocos glifos de una fuente de ~700 KB no pueden dar un stream de cientos de KB
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Para la salida PDF/A la comprobación es aún más precisa, porque un validador hace el trabajo por usted. Establezca el nivel de conformidad y pase el resultado por veraPDF: la falta de /CIDSet, o un subconjunto que no coincida con el descriptor, se reporta como una cláusula fallida en lugar de quedar para que lo note a simple vista. Los interruptores de conformidad que dirigen este trabajo de finalización son propiedades del documento. PDFACompliance toma una cadena como '2B' para PDF/A-2 Nivel B, y PDFUACompliance es un booleano que activa los requisitos de PDF etiquetado y orden de tabulación
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, impone la emisión de /CIDSet
Pdf.PDFUACompliance := True; // estampa /Tabs /S en páginas anotadas
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
La lección de ingeniería
De aquí se desprenden dos reglas. La primera es que cualquier paso de finalización que mute objetos tiene que ejecutarse antes de que esos objetos se serialicen, y la etapa de cierre de un motor de documentos debe leerse como una canalización ordenada donde la serialización es la última acción, no una acción entre varias. La segunda es la que más se olvida: para un paso de emisión, la ausencia de un error no es evidencia de éxito. Una rutina que construye el subconjunto correcto y lo aplica al gráfico equivocado, ya escrito, no reporta nada anómalo, porque desde su propia perspectiva no lo hay. La verificación tiene que mirar el artefacto, no el código de retorno. Compruebe el tamaño de la salida, lea la longitud en bytes de la fuente incrustada y su prefijo de /BaseFont, y deje que veraPDF juzgue la salida PDF/A, donde una falta de /CIDSet convierte un déficit silencioso en un fallo con nombre
El lado productor del manejo de fuentes, cómo se registran e incrustan las tipografías para la salida de informes, está cubierto en nuestro artículo sobre fuentes e imágenes en la salida de informes. El lado de validación, donde estos pasos de finalización se comprueban frente a los estándares, está cubierto en el recorrido sobre validación PDF/A y PDF/UA. Ambos se combinan con el trabajo de subconjunto y conformidad descrito aquí, que se distribuye como parte del HotPDF Delphi Component para Delphi y C++Builder junto con las APIs de carga, edición, cifrado y firma cubiertas en otros artículos de este blog