Genere un reporte, incruste una fuente TrueType y el resultado se abrirá correctamente en cualquier visor que pruebe. Los glifos serán correctos, el texto se podrá seleccionar y el archivo será válido. El único problema es el tamaño. Un documento que utilizó unas pocas docenas de caracteres latinos arrastra la fuente completa de 350 KB. Un documento que imprimió un párrafo en chino arrastra 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 número demasiado grande.
El error que lo produjo existió en HotPDF durante una línea de lanzamiento y ya se ha corregido. Vale la pena documentarlo no como un aviso de defecto, sino como una lección, porque la forma del error es general. Cualquier motor de documentos tiene una etapa de finalización que modifica objetos justo antes de escribirlos, y la corrección de esa etapa depende por completo del orden de sus pasos en relación con la serialización. Coloque un paso en el lado incorrecto de la escritura y no hará nada, en silencio.
Qué debería hacer la subdivisión de fuentes
Una fuente subdividida (subset font) 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 se transporta en un flujo referenciado por el descriptor de la fuente, y para un programa TrueType ese flujo es /FontFile2 con un /Length1 que indica el recuento de bytes sin comprimir. La subdivisión vuelve a escribir las tablas glyf y loca para que contengan únicamente los glifos que el documento referencia, cambia la numeración de 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 subdivide a diez o quince kilobytes es la diferencia entre un PDF optimizado y uno que incluye un tipo de letra completo solo por un encabezado.
El momento en que esto sucede es crucial. La subdivisión no es una transformación que se aplica a los bytes ya guardados en el disco. Edita el gráfico de objetos en memoria: reduce el contenido del flujo /FontFile2, corrige /Length1 y vuelve a escribir la cadena /BaseFont. Todo eso debe estar en su lugar cuando el serializador recorra el gráfico y emita los bytes. Si los cambios se realizan después de escribir los bytes, actualizarán objetos que nadie leerá jamás.
El síntoma y por qué no hubo advertencias
El comportamiento reportado consistía en fuentes completas en la salida sin ningún diagnóstico. Un usuario que registraba una fuente TrueType Unicode y generaba un documento normal descubría que el objeto de fuente incrustado tenía la misma longitud que el archivo .ttf de origen y que el nombre /BaseFont no incluía el prefijo de subconjunto de seis letras. El archivo de salida nunca reducía su tamaño entre ejecuciones que usaban diez glifos y aquellas que usaban diez mil.
La ausencia de errores es lo que hace que esta clase de errores sea costosa. Una rutina de subdivisión que se ejecuta en el momento incorrecto sigue funcionando. Recorre el uso acumulado de puntos de código, compila un subconjunto perfectamente correcto y lo aplica al gráfico de objetos en memoria. Internamente, el trabajo se realiza y la llamada finaliza limpiamente. El único problema es que el gráfico de objetos editado ya no es el elemento que se está escribiendo, porque el escritor ya terminó. Desde el punto de vista de la llamada, el documento se generó y guardó sin problemas, que es precisamente la impresión que da una falla silenciosa.
La causa raíz era el orden de finalización
En HotPDF, el trabajo de cierre ocurre dentro de EndDoc. El paso de subdivisión es una rutina interna llamada BuildAndApplyUnicodeFontSubset. Lee el conjunto de puntos de código utilizados por documento, almacenado 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 almacenada en caché de punto de código a glifo con un identificador de glifo real y vuelve a escribir 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 utilizados para cada carácter que dibuja, de modo que cuando el documento se cierra, el motor sabe exactamente qué glifos debe conservar el subconjunto.
El defecto era que BuildAndApplyUnicodeFontSubset se invocaba después de que SaveToStream o SaveToFile ya hubieran serializado el documento. Las ediciones del subdivisor en /FontFile2, su valor corregido /Length1 y el prefijo de seis letras en /BaseFont se calculaban en función de un gráfico de objetos que ya se había convertido en bytes. La solución fue reordenar una sola línea: mover la llamada de subdivisión antes de la serialización, para que el escritor emita la fuente subdividida en lugar de la original. La secuencia corregida ejecuta primero el subdivisor y luego serializa.
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; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Con el orden corregido, nada cambia en el código de llamada. La subdivisión está activada 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 utilizados antes de que los bytes salgan de la memoria.
Por qué un paso fuera de lugar representa toda una categoría
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 a una lista de pasos de cierre, y cada uno de ellos es sensible a su posición en relación con la escritura. La subdivisión de fuentes es uno. 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 declara el descriptor de la 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, por ISO 14289-1 §7.18.3, que cada página que contenga una anotación declare /Tabs con el valor /S, y una rutina interna llamada EnsurePDFUATabsOnAnnotatedPages sella esa clave durante la misma etapa. Las comprobaciones de intención de salida (output-intent) también se ejecutan allí.
El mismo error de ordenación que desactivó la subdivisión también eliminó la clave de orden de tabulación de PDF/UA en las páginas con anotaciones, porque ese paso se encontraba en el mismo lado incorrecto de la escritura. 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 fuera de lugar no solo infló el tamaño del archivo, sino que al mismo tiempo rompió en silencio un requisito de conformidad de accesibilidad, 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 un resultado exitoso.
Cómo se detecta realmente una falla de emisión silenciosa
Un error que no produce ninguna excepción no se detecta al ejecutar el programa. Se detecta al inspeccionar la salida y compararla con lo que debería haber producido la entrada. Para la subdivisión 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 flujo /FontFile2 subdividido 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, ya que 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;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
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: un flujo /CIDSet faltante, o un subconjunto que no coincida con el descriptor, se reportará como una cláusula con falla en lugar de esperar a 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, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
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 esto se desprenden dos reglas. La primera es que cualquier paso de finalización que modifique objetos debe 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 costó más tiempo aquí: para un paso de emisión, la ausencia de un error no es evidencia de éxito. Una rutina que compila el subconjunto correcto y lo aplica al gráfico incorrecto, ya escrito, no reporta problemas, porque desde su propia perspectiva no los hubo. La verificación debe centrarse en el artefacto, no en el código de retorno. Compruebe el tamaño de salida, lea la longitud en bytes de la fuente incrustada y su prefijo /BaseFont, y deje que veraPDF juzgue la salida PDF/A, donde un /CIDSet faltante convierte una deficiencia silenciosa en una falla identificada.
El lado de producción del manejo de fuentes, cómo se registran e incrustan los tipos de letra para la salida de reportes, se cubre en nuestro artículo sobre fuentes e imágenes en la salida de reportes. El lado de la validación, donde estos pasos de finalización se comprueban con los estándares, se cubre en la guía sobre validación de PDF/A y PDF/UA. Ambos se complementan con el trabajo de subdivisión y conformidad descrito aquí, que se incluye como parte de HotPDF Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras partes de este blog.