PDFium Component subconjunta fuentes que ya están incrustadas en un PDF existente a través de SubsetPdfDocumentFonts en la unidad FPdfDocumentFontSubset. La función reemplaza un programa /FontFile2 TrueType incrustado por uno más pequeño solo cuando los recursos de la propia fuente prueban cada glifo que el documento puede referenciar; una fuente cuyo dominio de glifos no se puede probar se deja exactamente como estaba
La razón habitual para querer esto es un archivo que usted no produjo. Las facturas, los estados de cuenta y los reportes de generadores viejos suelen llevar un Arial completo o una fuente CJK completa en cada archivo, y unos cuantos cientos de kilobytes de fuente por documento de dos páginas se acumulan rápido a lo largo de unos millones de documentos. Volver a renderizar desde el origen rara vez es una opción. Lo que usted tiene 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 escritor 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 se le escape se convierte en un recuadro vacío o 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 apariencia de anotación y cada apariencia por defecto de campo, recolectar los códigos mostrados con cada fuente, y conservar esos glifos. Funciona hasta que se topa con un recurso de fuente compartido entre páginas y forms, un campo de formulario cuya apariencia 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 arreglos de anchos /W y /W2, el bitmap /CIDSet, la CMap /ToUnicode, el arreglo /Widths de fuente simple y el /Encoding. Cuando esos metadatos no acotan el dominio, la fuente queda intacta
Conservar los IDs de glifo originales para que los content streams queden intactos
El backend de subconjuntos detrás de SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, conserva cada hueco de ID de glifo original, así que ningún content stream, CID ni /CIDToGIDMap tiene que reescribirse jamás. Reconstruye las tablas glyf y loca, le da a cada glifo sin usar un rango loca de largo cero y deja maxp.numGlyphs sin cambios. El glifo 4711 sigue siendo el glifo 4711 tras el subconjuntado; solo que tiene contornos si el documento puede alcanzarlo
Esa elección elimina la parte más riesgosa de subconjuntar un documento terminado. Renumerar glifos significaría reescribir cada cadena en cada content stream que use la fuente, más el stream de mapeo, más cualquier cosa que cachee IDs de glifo. Los IDs retenidos cuestan un poco de tamaño, porque loca guarda una entrada por glifo original, pero el grueso de un programa TrueType son datos de contorno, 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 con el que se construye; las referencias compuestas cíclicas o demasiado profundas hacen fallar la fuente en lugar de producir un cierre roto
La fuente renombrada también sigue ISO 32000-1 §9.6.4, que exige que el nombre de una fuente de subconjunto empiece con 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 primero cualquier prefijo ABCDEF+ existente, resuelve colisiones entre fuentes del mismo documento, y escribe el nuevo nombre 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 siempre es un PDF completo: el archivo reconstruido cuando Report.Applied
// es True, de lo contrario 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 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 en el stream entra al dominio. Un stream de mapeo de más de 65536 entradas, o uno con conteo de bytes impar, hace fallar el perfil acotado y la fuente se preserva
/CIDToGIDMap /Identity es el caso difícil, y también el más común en archivos modernos. Identity dice que CID es igual a GID, lo que no le dice nada sobre qué CIDs usa el documento. El componente por tanto exige evidencia primaria: un arreglo 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 de los dos se preserva con la razón Identity CIDToGIDMap requires an explicit W array or CIDSet stream
La trampa de usar /W como evidencia es /DW. A un productor se le permite omitir todo CID cuyo ancho sea igual al ancho por defecto, y un escritor bien optimizado hace exactamente eso con los anchos de glifo más comunes. Si el dominio viniera solo de /W, esos glifos desaparecerían silenciosamente del subconjunto. Para cerrar ese hueco 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 mezcla los códigos de origen de cada entrada beginbfchar y beginbfrange de su CMap /ToUnicode (§9.10.3) dentro del dominio. 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 IDs de glifo
Las fuentes de un byte /Subtype /TrueType se prueban a través de su rango de códigos y su codificación, y luego se resuelven a IDs de glifo por el cmap del propio programa de fuente. El componente exige 0 <= FirstChar <= LastChar <= 255 y un arreglo /Widths cuyo largo sea exactamente LastChar - FirstChar + 1; un arreglo 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 en 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 hacia 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 toda interpretación plausible en lugar de adivinar una. Eso conserva unos cuantos glifos que probablemente no se usan, que es la dirección correcta para equivocarse. El caso opuesto es estricto: un nombre de /Differences que no mapea a ningún ID de glifo probado rechaza toda la fuente, 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; // archivo completo
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 intacto SubsetPdfDocumentFonts?
SubsetPdfDocumentFonts deja intacta cada fuente que no puede probar, y reporta cada una en Report.Issues con el número de objeto y una razón en lugar de hacer fallar el documento. Report.Complete es True solo cuando ninguna fuente se preservó. Los casos preservados valen conocerlos antes de prometer una reducción de tamaño:
- Programas Type 1
/FontFiley CFF u OpenType/FontFile3, reportados 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 otro lugar 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 archivo 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 reemplazar objetos de fuente invalidaría el rango de bytes de cada firma, y los archivos cifrados necesitan primero un origen descifrado. Una corrida exitosa escribe los objetos de reemplazo como una actualización dispersa y luego reconstruye el archivo a través de la misma pasada de alcanzabilidad descrita en la guía para podar objetos huérfanos y deduplicar recursos, así 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 se mapea al flag de PDFium FPDF_SUBSET_NEW_FONTS, que corre el subsetter nativo de HarfBuzz sobre las fuentes que llamadas nativas como TPdf.AddText cargaron del sistema, incluidas las caras CFF de OpenType. El PDFium de origen todavía marca el flag como experimental. No toca las fuentes que ya estaban en el archivo
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 archivo guardado siguió coincidiendo exactamente. Una fuente de color COLR v0, Segoe UI Emoji de 7.0 MB, pasó por la misma ruta hasta 10.5 KB, renderizada como sus contornos base ya que PDF no tiene noción de capas COLR. Las fuentes variables son la excepción, y el bloque se pone delante del subsetter: el GetFontData de GDI devolvió un stream sustituto de 18.3 MB para el archivo Segoe UI Variable de 1.85 MB, y la creación del objeto de texto falló antes de que comenzara 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 el productor escribió. Un CIDFont Identity cuya evidencia es solo /W todavía tiene un punto ciego: un glifo cuyo ancho es igual a /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, renderice una muestra antes y después y compare píxeles y texto extraído
Antes de correr un lote también ayuda saber con qué está lidiando. El enfoque de leer propiedades de fuentes PDF con PDFium Component le dice qué fuentes están incrustadas siquiera, y el Report.Issues de una corrida en seco le 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
¿SubsetPdfDocumentFonts cambia el texto o el layout del PDF?
No. Los IDs de glifo originales, los CIDs, los anchos y los content streams 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 nuevo prefijo de subconjunto
¿Por qué una fuente incrustada no se subconjuntó?
Lea el Report.Issues. Cada fuente preservada lleva una razón como un /W o /CIDSet faltante en un CIDFont Identity, un programa no TrueType, un arreglo /Widths malformado o un ahorro por debajo del umbral
¿saSubsetNewFonts es lo mismo que SubsetPdfDocumentFonts?
No. saSubsetNewFonts subconjunta fuentes incrustadas en la sesión actual al guardar, mientras que SubsetPdfDocumentFonts trabaja sobre un stream PDF existente y reemplaza fuentes que otro productor ya había incrustado
El subconjuntado de fuentes de documento, el subconjuntado de fuentes al guardar y la pasada de poda de huérfanos detrás de todos ellos vienen con el PDFium Component for Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus