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
El error que lo produjo existió en HotPDF durante una línea de lanzamiento y ya se ha corregido. Vale la pena describirlo 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 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 síntoma y por qué nada se quejó
El comportamiento reportado era la presencia de 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 de /BaseFont no llevaba el prefijo de subconjunto de seis letras. La salida nunca se reducía entre ejecuciones que utilizaban diez glifos y ejecuciones que utilizaban 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
La causa raíz fue 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
El defecto era que BuildAndApplyUnicodeFontSubset se invocaba después de que SaveToStream o SaveToFile ya hubieran serializado el documento. Las ediciones del creador de subconjuntos en /FontFile2, su /Length1 corregido y el prefijo de seis letras de /BaseFont se calcularon sobre 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 al subconjunto antes de la serialización, para que el escritor emita la fuente en subconjunto en lugar de la original. La secuencia corregida ejecuta primero el creador de subconjuntos y serializa despué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; // 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. 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 desactivó el subconjunto también omitió la clave de orden de tabulación PDF/UA en las páginas anotadas, 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 mal colocada no solo infló el tamaño del archivo; rompió 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;
// 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: 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, 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 mute objetos tiene que ejecutarse antes de que esos objetos se serialicen, y la etapa de cierre de un motor de documentos debe interpretarse como una tubería 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 crea el subconjunto correcto y lo aplica a un gráfico incorrecto, ya escrito, no reporta ningún problema, porque desde su propia perspectiva no hubo ninguno. La verificación tiene que mirar el artefacto, no el código de retorno. Compruebe el tamaño de salida, lea la longitud de bytes de la fuente incrustada y su prefijo de /BaseFont, y deje que veraPDF juzgue la salida PDF/A donde la falta de /CIDSet convierte una deficiencia silenciosa en un fallo identificado
La parte de la gestión de fuentes relacionada con la producción, sobre cómo se registran e incrustan los tipos de letra para la salida de informes, se detalla en nuestro artículo sobre fuentes e imágenes en la salida de informes. El lado de la validación, donde se comprueban estos pasos de finalización frente a los estándares, se cubre en la guía práctica sobre validación PDF/A y PDF/UA. Ambos se complementan con el trabajo de subconjunto y conformidad descrito aquí, que se distribuye como parte de HotPDF Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras secciones de este blog