PDFium Component subconjunta fuentes que ya están incrustadas en un PDF existente mediante SubsetPdfDocumentFonts en la unidad FPdfDocumentFontSubset. La función sustituye un programa /FontFile2 TrueType incrustado por uno más pequeño solo cuando los propios recursos de la fuente prueban cada glifo que el documento puede referenciar; una fuente cuyo dominio de glifos no se puede probar se queda exactamente como estaba
El motivo habitual para querer esto es un archivo que no produjisteis vosotros. Las facturas, extractos e informes de generadores antiguos suelen llevar una Arial completa o una cara CJK entera en cada fichero, y unos cientos de kilobytes de fuente por documento de dos páginas se acumulan rápido entre unos cuantos millones de documentos. Re-renderizar desde el origen rara vez es una opción. Lo que tenéis es el PDF, y la única pregunta segura es cuánto de cada fuente incrustada puede irse sin que una sola página pierda un carácter
Por qué subconjuntar un PDF existente arriesga perder glifos
Subconjuntar un PDF existente arriesga perder glifos porque el postprocesador no sabe qué glifos usó el writer original. El productor que incrustó la fuente conocía su propio texto; una herramienta que corre después tiene que reconstruir ese conocimiento, y cualquier glifo que pierda se convierte en una caja vacía o en una forma .notdef en una página que nadie mira hasta que la mira un cliente
El enfoque obvio es parsear cada content stream, cada Form XObject, cada appearance de anotación y cada default appearance de campo, recoger los códigos mostrados con cada fuente, y conservar esos glifos. Funciona hasta que se encuentra con un recurso de fuente compartido entre páginas y forms, un campo de formulario cuyo appearance un visor regenera desde /DA, o un content stream con una secuencia de operadores que el escáner no modela. SubsetPdfDocumentFonts deliberadamente no escanea content streams. Deriva el dominio de glifos de los metadatos que el PDF tiene que llevar de todos modos sobre cada fuente: el /CIDToGIDMap, los arrays de anchos /W y /W2, el bitmap /CIDSet, la CMap /ToUnicode, el array /Widths de fuente simple y el /Encoding. Cuando esos metadatos no acotan el dominio, la fuente se queda intacta
Conservar los glyph IDs originales para que los content streams no se toquen
El backend de subconjuntos detrás de SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, conserva cada hueco de glyph ID original, así que ningún content stream, CID ni /CIDToGIDMap tiene que reescribirse jamás. Reconstruye las tablas glyf y loca, da a cada glifo sin usar un rango loca de longitud cero y deja maxp.numGlyphs sin cambios. El glifo 4711 sigue siendo el glifo 4711 después del subconjunto; solo tiene contornos si el documento puede alcanzarlo
Esa elección quita la parte más arriesgada de subconjuntar un documento terminado. Renumerar glifos significaría reescribir cada string de cada content stream que usa la fuente, más el stream de mapeo, más cualquier cosa que cachee glyph IDs. Los IDs retenidos cuestan algo de tamaño, porque loca mantiene una entrada por glifo original, pero el grueso de un programa TrueType son datos de contornos, y eso es lo que se va. Los glifos compuestos se siguen hasta sus componentes, así que una letra acentuada conserva la letra base y el acento del que está hecha; las referencias compuestas cíclicas o demasiado profundas hacen fallar la fuente en lugar de producir un cierre roto
La fuente renombrada sigue además ISO 32000-1 §9.6.4, que exige que el nombre de una fuente de subconjunto empiece por seis letras mayúsculas y un signo más. El componente deriva las seis letras de un digest SHA-256 del nuevo programa de fuente, quita antes cualquier prefijo ABCDEF+ existente, resuelve colisiones entre fuentes del mismo documento, y escribe el nombre nuevo tanto en el /FontName del descriptor como en el /BaseFont del diccionario de fuente, incluido el padre Type0 de un CIDFont
uses
System.SysUtils, System.Classes, FPdfDocumentFontSubset;
procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
Source, Target: TFileStream;
Options: TPdfDocumentFontSubsetOptions;
Report: TPdfDocumentFontSubsetReport;
Output: TBytes;
begin
Options := TPdfDocumentFontSubsetOptions.Default;
Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
try
if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
finally
Source.Free;
end;
// La salida es siempre un PDF completo: el fichero reconstruido cuando Report.Applied
// es True, si no una copia byte a byte del origen
Target := TFileStream.Create(TargetName, fmCreate);
try
if Length(Output) > 0 then
Target.WriteBuffer(Output[0], Length(Output));
finally
Target.Free;
end;
end;
Cómo prueba SubsetPdfDocumentFonts el dominio de glifos de un CIDFont
SubsetPdfDocumentFonts prueba el dominio de glifos de una fuente CIDFontType2 a partir de su mapeo de CID a glifo, y el mapeo decide cuánta evidencia extra hace falta. Un stream /CIDToGIDMap explícito (ISO 32000-1 §9.7.4.2) ya es una lista de cada glifo que la fuente puede direccionar, así que cada GID del stream entra en el dominio. Un stream de mapeo de más de 65536 entradas, o uno con recuento de bytes impar, falla el perfil acotado y la fuente se preserva
/CIDToGIDMap /Identity es el caso duro, y también el más común en ficheros modernos. Identity dice que CID igual a GID, lo que no os dice nada sobre qué CIDs usa el documento. El componente exige por tanto evidencia primaria: un array de anchos /W o /W2 explícito (§9.7.4.3), cuyas entradas CID marcan glifos para los que el productor declaró métricas, o un stream /CIDSet en el descriptor de fuente (§9.8.3), un bitmap con un bit por CID, bit más significativo primero. Una fuente Identity pelada sin ninguno se preserva con el motivo Identity CIDToGIDMap requires an explicit W array or CIDSet stream
La trampa al usar /W como evidencia es /DW. A un productor se le permite omitir cada CID cuyo ancho iguale el ancho por defecto, y un writer bien optimizado hace exactamente eso con los anchos de glifo más comunes. Si el dominio viniera solo de /W, esos glifos desaparecerían calladamente del subconjunto. Para cerrar esa brecha el componente recorre cada fuente Type0 cuyo /DescendantFonts apunta al CIDFont, exige que su /Encoding sea /Identity-H o /Identity-V para que un código de 2 bytes sea el CID, y fusiona en el dominio los códigos de origen de cada entrada beginbfchar y beginbfrange de su CMap /ToUnicode (§9.10.3). Cualquier otra codificación, o un stream /ToUnicode que no se pueda decodificar dentro del presupuesto, preserva la fuente
Fuentes TrueType simples: de Widths a glyph IDs
Las fuentes /Subtype /TrueType de un solo byte se prueban a través de su rango de códigos y su codificación, y luego se resuelven a glyph IDs por el cmap del propio programa de fuente. El componente exige 0 <= FirstChar <= LastChar <= 255 y un array /Widths cuya longitud es exactamente LastChar - FirstChar + 1; un array corto, largo o no numérico significa que el dominio de códigos no está probado, y la fuente se queda como está
Cada código de ese rango, más cualquier código nombrado en /Differences o cubierto por /ToUnicode, se mapea luego como describe ISO 32000-1 §9.6.6. Los códigos bajo /WinAnsiEncoding, /MacRomanEncoding o /StandardEncoding pasan por la tabla correspondiente hasta Unicode. Los nombres de glifo de /Differences se resuelven por la Adobe Glyph List, los nombres uniXXXX y uXXXX, los 258 nombres de glifo estándar de Macintosh y la tabla de nombres post formato 2.0 de la propia fuente. Los valores resultantes se buscan en subtablas cmap de formato 0, 4, 6 y 12, incluidas Unicode (3,1), (3,10) y (0,*), Mac Roman (1,0), y Windows Symbol (3,0) en F000 más el código
Cuando una fuente no tiene ninguna codificación base, el componente marca la unión de cada interpretación plausible en lugar de adivinar una. Eso conserva unos pocos glifos que probablemente no se usan, que es la dirección correcta en la que equivocarse. El caso opuesto es estricto: un nombre de /Differences que mapea a ningún glyph ID probado rechaza la fuente entera, porque un nombre sin resolver significa que el dominio está incompleto
procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
Options: TPdfDocumentFontSubsetOptions;
Report: TPdfDocumentFontSubsetReport;
Output: TBytes;
I: Integer;
begin
Options := TPdfDocumentFontSubsetOptions.Default;
Options.SubsetOptions.MinimumSavingPercent := 10; // por fuente
Options.MinimumDocumentSavingPercent := 5; // fichero entero
if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
begin
Log.Add('Rejected: ' + Report.ErrorMessage);
Exit;
end;
Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
[Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
Report.GlyphCount]));
Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
[Report.OriginalFontBytes, Report.SubsetFontBytes,
Report.SourceByteCount, Report.OutputByteCount]));
for I := 0 to High(Report.Issues) do
Log.Add(Format(' object %d kept: %s',
[Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;
Qué deja solo SubsetPdfDocumentFonts
SubsetPdfDocumentFonts deja solo cada fuente que no puede probar, y informa cada una en Report.Issues con el número de objeto y un motivo en lugar de fallar el documento. Report.Complete es True solo cuando no se preservó ninguna fuente. Los casos preservados conviene conocerlos antes de prometer una reducción de tamaño:
- Programas Type 1
/FontFiley CFF u OpenType/FontFile3, informados comoOnly TrueType FontFile2 programs are supported - CIDFonts Identity sin
/W,/W2ni/CIDSet, y CIDFonts sin stream de mapeo ni/Identity - Programas de fuente referenciados desde algún sitio distinto de sus descriptores de fuente, y descriptores compartidos con un consumidor que no es
CIDFontType2niTrueTypesimple - Fuentes cuyo subconjunto no alcanza
SubsetOptions.MinimumSavingPercent, y todas las fuentes cuando el fichero reconstruido no alcanzaMinimumDocumentSavingPercent; en ese casoReport.AppliedesFalsey la salida son los bytes originales
Dos entradas fallan de plano en lugar de preservarse. Los documentos firmados y los documentos con campos de firma se rechazan, porque sustituir objetos de fuente invalidaría el rango de bytes de cada firma, y los ficheros cifrados necesitan primero un origen descifrado. Una ejecución con éxito escribe los objetos de sustitución como una actualización dispersa y luego reconstruye el fichero mediante la misma pasada de alcanzabilidad descrita en la guía de podar objetos huérfanos y deduplicar recursos, así que los programas de fuente viejos no sobreviven como objetos inalcanzables y el resultado es una única revisión limpia
Subconjuntar texto nuevo al guardar con saSubsetNewFonts
Las fuentes que PDFium Component incrusta durante la sesión actual son un caso aparte, manejado por la opción de guardado saSubsetNewFonts en lugar de por SubsetPdfDocumentFonts. saSubsetNewFonts mapea al flag FPDF_SUBSET_NEW_FONTS de PDFium, que ejecuta el subsetter nativo HarfBuzz sobre fuentes que llamadas nativas como TPdf.AddText cargaron del sistema, incluidas caras OpenType CFF. PDFium upstream sigue etiquetando el flag como experimental. No toca fuentes que ya estaban en el fichero
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(0, 595, 842);
Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
En la sonda del componente, un documento con texto Arial bajó de 590 KB a 22 KB bajo saSubsetNewFonts, y la extracción de texto del fichero guardado seguía casando exactamente. Una fuente de color COLR v0, Segoe UI Emoji de 7.0 MB, pasó por la misma ruta hasta 10.5 KB, renderizada con sus contornos base ya que PDF no tiene noción de capas COLR. Las fuentes variables son la excepción, y el bloque está delante del subsetter: GDI GetFontData devolvió un stream sustituto de 18.3 MB para el fichero Segoe UI Variable de 1.85 MB, y la creación de objetos de texto falló antes de que empezara cualquier subconjuntado. El recorrido de la incrustación de fuentes CID Type 2 desde Delphi cubre la ruta de incrustación que esas fuentes atraviesan primero
Dónde se detiene la prueba
La prueba dentro de SubsetPdfDocumentFonts es exactamente tan fuerte como los metadatos de fuente que escribió el productor. Un CIDFont Identity cuya evidencia es solo /W todavía tiene un punto ciego: un glifo cuyo ancho iguala /DW, que no tiene entrada /ToUnicode, y que el descriptor no lista en un /CIDSet. Tal glifo no es demostrable desde los recursos, y el componente no puede saber que existe sin el escaneo de content streams que evita a propósito. Los productores que omiten tanto /ToUnicode como /CIDSet mientras comprimen /W son raros, pero existen, así que para un archivo de un generador desconocido, renderizad una muestra antes y después y comparad píxeles y texto extraído
Antes de lanzar un lote también ayuda saber con qué estáis tratando. El enfoque de leer propiedades de fuentes PDF con PDFium Component os dice qué fuentes están incrustadas siquiera, y Report.Issues de una pasada en seco os dice cuáles de ellas se preservarán y por qué. Las opciones por defecto también acotan el trabajo: 1024 fuentes, cerca de un millón de glifos en total, 128 MB de origen y un presupuesto de trabajo fijo, con un CancellationToken opcional para trabajos por lotes largos
FAQ
¿Cambia SubsetPdfDocumentFonts el texto o el layout del PDF?
No. Los glyph IDs, CIDs, anchos y content streams originales se conservan, así que la extracción de texto y el renderizado usan los mismos glifos que antes; solo se quitan los contornos inalcanzables y los nombres de fuente reciben un prefijo de subconjunto nuevo
¿Por qué no se subconjuntó una fuente incrustada?
Leed Report.Issues. Cada fuente preservada lleva un motivo como un /W o /CIDSet ausente en un CIDFont Identity, un programa que no es TrueType, un array /Widths malformado o un ahorro por debajo del umbral
¿Es saSubsetNewFonts lo mismo que SubsetPdfDocumentFonts?
No. saSubsetNewFonts subconjunta las fuentes incrustadas en la sesión actual cuando guardáis, mientras que SubsetPdfDocumentFonts trabaja sobre un stream PDF existente y sustituye fuentes que otro productor ya había incrustado
El subconjuntado de fuentes de documento, el subconjuntado al guardar y la pasada de poda de huérfanos detrás de todos ellos vienen con el PDFium Component para Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus