Genere un reporte, incruste una fuente TrueType y la salida se abre correctamente en cada visor que pruebe. Los glifos son correctos, el texto es seleccionable, el archivo es válido. Lo único malo es el tamaño. Un documento que utilizó un par de docenas de caracteres latinos transporta la fuente completa de 350 KB. Un documento que imprimió 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 generó ninguna excepción, no se registró ninguna advertencia y el archivo pasó la validación. Así es como se ve desde afuera un paso de finalización mal ordenado: nada falla, y la única evidencia es un número que es demasiado grande
El error que lo produjo vivió en HotPDF durante una línea de lanzamiento y desde entonces ha sido solucionado. Vale la pena escribirlo 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 exactitud de esa etapa depende por completo del orden de sus pasos relativos a la serialización. Coloque un paso en el lado incorrecto de la escritura y no hará nada, silenciosamente
Lo que se supone que debe hacer la creación de subconjuntos de fuentes
Una fuente de subconjunto es la parte de un archivo TrueType que un documento realmente utiliza. ISO 32000-1 §9.9 describe cómo un programa de fuente incrustado se desplaza en un flujo (stream) al que hace referencia el descriptor de fuente, y para un programa TrueType ese flujo es /FontFile2 con un /Length1 que da el recuento de bytes sin comprimir. La creación de subconjuntos reescribe las tablas glyf y loca de modo que contengan solo los glifos a los que hace referencia el documento, vuelve a numerar los identificadores de glifos y antepone al nombre /BaseFont una etiqueta de seis letras como ABCDEF+ para marcar la fuente como un subconjunto, exactamente como lo requiere la especificación. Una tipografía latina que se reduce a diez o quince kilobytes es la diferencia entre un PDF liviano y uno que envía una tipografía completa por el simple hecho de un encabezado
El momento en el que esto ocurre importa. La creación de subconjuntos no es una transformación que se aplica a bytes ya en el disco. Edita el gráfico de objetos en memoria: encoge el contenido del flujo /FontFile2, ajusta el /Length1 y reescribe la cadena /BaseFont. Todo eso tiene que estar en su lugar cuando el serializador recorre el gráfico y emite los bytes. Si las ediciones aterrizan después de que se escriben los bytes, actualizan objetos que nadie jamás leerá
El síntoma, y por qué nada se quejó
El comportamiento reportado fueron fuentes completas en la salida sin ningún diagnóstico. Un usuario que registró una fuente TrueType Unicode y produjo un documento normal descubrió 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 ningún prefijo de subconjunto de seis letras. La salida nunca se redujo entre ejecuciones que usaron diez glifos y ejecuciones que usaron diez mil
La ausencia de cualquier error es la parte que hace que esta clase de error sea costosa. Una rutina de creación de subconjuntos que se ejecuta en el momento incorrecto aún se ejecuta. Recorre el uso de puntos de código (codepoints) acumulado, construye un subconjunto perfectamente correcto y lo aplica al gráfico de objetos en memoria. Internamente, el trabajo está hecho y la llamada retorna limpiamente. Lo único malo es que el gráfico de objetos que editó ya no es lo que se está escribiendo, porque el escritor ya terminó. Desde el punto de vista de la persona que llama, el documento se produjo 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 creación de subconjuntos es una rutina interna llamada BuildAndApplyUnicodeFontSubset. Lee el conjunto de puntos de código utilizados 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 utilizado a través de la tabla en caché de punto-de-código-a-glifo 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 utilizados para cada carácter que dibuja, por lo que, en el momento en que se cierra el documento, el motor sabe exactamente qué glifos debe mantener el subconjunto
El defecto fue que BuildAndApplyUnicodeFontSubset se invocaba después de que SaveToStream o SaveToFile ya hubieran serializado el documento. Las ediciones del creador de subconjuntos a /FontFile2, su /Length1 corregido y el prefijo de seis letras /BaseFont se computaban contra un gráfico de objetos que ya se había convertido en bytes. La solución fue un reordenamiento de una línea: mover la llamada del subconjunto por delante de la serialización, de modo 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 acerca del código de llamada cambia. La creación de subconjuntos está activada de forma predeterminada una vez que se ha registrado una fuente TrueType Unicode. Usted registra la fuente, comienza el documento, dibuja y lo termina, y el subconjunto se construye a partir de los glifos que utilizó antes de que los bytes salgan de la memoria
Por qué un paso fuera de lugar es toda una categoría
La razón por la que esto vale 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. La creación de subconjuntos de fuentes es uno. La salida PDF/A requiere un flujo /CIDSet que enumere exactamente los identificadores de glifos presentes en el subconjunto, una restricción que impone ISO 19005 para que un validador pueda confirmar que el programa incrustado coincida 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 construido primero. PDF/UA-1 requiere, por 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 sella esa clave durante la misma etapa. Las verificaciones de intenciones de salida (output-intent) también se ejecutan allí
La misma falla de ordenamiento que deshabilitó la creación de subconjuntos también descartó la clave de orden de tabulación de PDF/UA en las páginas anotadas, porque ese paso se encontraba en el mismo lado incorrecto de la escritura. veraPDF y PAC reportan un /Tabs /S faltante como una violación del punto de control 21-001 del protocolo Matterhorn. Por lo tanto, una sola llamada fuera de lugar no se limitó a inflar el tamaño del archivo; de forma silenciosa rompió un requisito de conformidad de accesibilidad al mismo tiempo, con la misma ausencia de error alguno. Ese es el peligro de una etapa de finalización: sus pasos comparten una precondición, y un solo error de ordenamiento puede eliminar varios de ellos a la vez mientras que cada llamada aún devuelve éxito
Cómo se detecta realmente un fallo de emisión silencioso
Un error que no genera una excepción no se detecta ejecutando el programa. Se detecta inspeccionando la salida y comparándola con lo que debería haber producido la entrada. Para la creación de subconjuntos de fuentes, los controles son concretos. Compare el tamaño del archivo de salida con una expectativa aproximada: un documento que tocó un puñado de glifos no debería ser del tamaño de una tipografía completa. Abra el objeto de fuente incrustado y lea su longitud en bytes; un /FontFile2 reducido para una tipografía latina es una pequeña fracción del archivo de origen. Lea el nombre /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 verificación es aún más nítida, porque un validador hace el trabajo por usted. Establezca el nivel de conformidad y pase el resultado por veraPDF: un /CIDSet faltante, o un subconjunto que no coincide con el descriptor, se reporta como una cláusula fallida en lugar de dejárselo a usted para que lo note a simple vista. Los interruptores de conformidad que impulsan 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 debería 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 construye el subconjunto correcto y lo aplica al gráfico incorrecto, ya escrito, no reporta nada incorrecto, porque desde su propia perspectiva nada lo fue. La verificación tiene que observar el artefacto, no 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 un defecto silencioso en un fallo con nombre
El lado del productor del manejo de fuentes, cómo se registran e incrustan las tipografías para la salida de reportes, está cubierto 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 verifican contra los estándares, está cubierto en el recorrido sobre validación de PDF/A y PDF/UA. Ambos se emparejan con el trabajo de creación de subconjuntos y conformidad descrito aquí, que se incluye como parte del HotPDF Component para Delphi y C++Builder junto con las API de carga, edición, encriptación y firma que se cubren en otras partes de este blog