PDF Library for Delphi elimina un operador de text matrix identidad, 1 0 0 1 0 0 Tm, durante su optimización peephole de content streams al guardar solo cuando la text matrix y la text line matrix ya son la identidad: justo después de BT, o justo después de un Tm identidad anterior. Un cm identidad se sigue descartando siempre, porque cm multiplica la CTM mientras que Tm sustituye ambas text matrices de plano. Desde v3.539.28, cualquier otro Tm identidad se queda en el stream
El bug que esto corrige es de los silenciosos. Un generador de informes emite BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, confiando en el Tm identidad para devolver la segunda cadena al origen del text space antes de aplicar su propia lógica de posicionamiento. El optimizador antiguo veía seis números que deletreaban la matriz identidad, decidía que el operador no podía cambiar nada, y lo borraba. Nada falló, nada registró una advertencia, y la página guardada dibujaba «Total» inmediatamente después de «Invoice» sobre la misma línea base, que es justo la clase de defecto que nadie nota hasta que un cliente imprime el PDF
¿Por qué 1 0 0 1 0 0 Tm no es siempre un no-op?
Un Tm identidad solo es un no-op cuando sustituiría dos matrices que ya contienen la identidad, y eso es una propiedad de los operadores que lo preceden, no de sus propios operandos. ISO 32000-1 §9.4.1 dice que BT inicializa tanto la text matrix (Tm) como la text line matrix (Tlm) a la identidad, y §9.4.2 define Tm como asignarles a ambas los valores dados, no concatenar sobre ellas. Compare eso con cm (§8.4.4), que multiplica por la derecha la current transformation matrix: multiplicar por la identidad deja cualquier CTM intacta, así que 1 0 0 1 0 0 cm se puede borrar en cualquier sitio. Dentro de un objeto de texto el panorama cambia. Td, TD, T* y un Tm no identidad mueven todos la Tlm, y cada operador que muestra texto (Tj, TJ, ', ") avanza la Tm el ancho de los glifos que pintó. Después de cualquiera de esos, un Tm identidad es un reset de verdad al origen. Si alguna vez ha trazado posiciones de texto a mano con el rastreador de estado de CTM y text matrix del content stream, es la misma distinción entre concatenar estado y sustituirlo
Cómo decide el escaneo hacia atrás qué Tm identidad descartar
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices camina ahora hacia atrás desde cada Tm identidad y solo lo descarta si el escaneo llega antes a BT o a otro Tm identidad. El Tm identidad anterior cuenta tanto si se conserva como si acaba de quedar programado para borrarse, porque en ambos casos dejó ambas matrices en la identidad, igual que hace BT. La regla reparte cada operador que puede encontrarse en uno de dos grupos:
- Parar y conservar el Tm:
Td,TD,T*, unTmno identidad,Tj,TJ,',",ET, cualquier operador que el parser no reconozca, o el inicio del stream - Saltarse y seguir escaneando: operadores que jamás tocan Tm ni Tlm, como
Tf,Tc, los que fijan color,gs, los operadores de marked-content ycm
Los casos conservadores son deliberados. Un operador desconocido puede ser cualquier cosa, así que el escaneo se niega a razonar más allá. ET cierra el objeto de texto, así que un Tm posterior no tiene ningún BT que avale los valores de la matriz. El escaneo además trabaja de una en una los content streams, lo que importa en páginas cuyo /Contents es un array: una capa que empieza en mitad de un objeto de texto, sin BT propio, conserva su Tm identidad aunque la capa anterior la habría vuelto redundante. Cuesta unos bytes en archivos raros y nunca mueve un glifo. Si edita texto de página al nivel de instrucción, como en el recorrido de mapeo carácter a byte de contenido, el mismo modelo TPDFContentProgram parseado es lo que reescribe el optimizador
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // stream dañado: dejar los bytes en paz
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // devuelve el número de instrucciones eliminadas
finally
Optimizer.Free;
end;
Result := Prog.Emit; // una instrucción por línea
finally
Prog.Free;
end;
end;
// Eliminado: Tm justo después de BT, el segundo de dos Tm identidades seguidos
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Conservado: Tm tras Td, tras Tj, tras un Tm no identidad, o fuera de BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Ejecute el helper sobre el stream de la factura del principio y el Tm identidad sobrevive, porque el escaneo hacia atrás se topa con Tj antes de llegar a BT. Ponga /F1 12 Tf, 2 Tc y 0 g entre BT y el Tm identidad y todavía se va, porque ninguno de esos toca las text matrices. Una secuencia como BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm pierde exactamente un operador: el primer Tm identidad reinicia la matriz que Td movió, y solo el segundo es redundante
¿Cuándo se ejecuta de verdad el optimizador peephole?
El optimizador solo corre durante el pase de compresión, dentro de TPDFPageTree.Compress, y solo sobre content streams que aún no están comprimidos con Flate. TPDFlib.SetOptimizeContentStreams(1) es el valor por defecto, y el mismo interruptor se expone como el campo OptimizeContentStreams de TPDFlibSaveOptions; tanto CompressContent como CompressPage lo respetan. Un stream cuyo /Filter ya es /FlateDecode se salta por completo, así que cargar un PDF comprimido existente y volverlo a guardar no reescribe sus operadores. Si el stream no se puede parsear, los bytes decodificados originales se comprimen sin cambios. TPDFlib.NormalizeContentStreams parsea y vuelve a emitir contenido con espaciado y números canónicos pero nunca llama al optimizador, lo que lo convierte en una línea base útil cuando quiere ver cuánta diferencia de tamaño aportan las reglas peephole, junto a las ganancias mayores cubiertas en la optimización de tamaño de archivos PDF con subsetting de fuentes
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Los streams sin comprimir pasan por las reglas peephole, luego Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// La misma elección por las save options incluidas; False la desactiva
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
¿Qué garantizaba de verdad el antiguo test de regresión?
El antiguo test de regresión garantizaba una sola forma: un Tm identidad justo después de BT se elimina. Peephole_RemovesIdentityTextMatrix alimenta al optimizador con BT 1 0 0 1 0 0 Tm (hello) Tj ET y afirma que no queda ningún Tm. Una versión anterior ya había señalado que descartar un Tm identidad no es seguro cuando la Tlm no es la identidad, y aun así mantuvo el comportamiento porque el test lo «bloqueaba». Leído con cuidado, el test no dice nada sobre un Tm identidad tras Td o tras texto mostrado; tomar la cobertura de una muestra como el contrato de toda la regla era el error real. El fix mantiene ese caso original en verde y añade seis casos que fijan tanto las formas eliminables como las conservadas, incluido un Tm fuera de todo objeto de texto y otro que va detrás de ET
El trade-off es fácil de aceptar una vez puesto por escrito. Los generadores que envuelven cada objeto de texto como BT 1 0 0 1 0 0 Tm ... siguen viendo cómo se elimina ese operador redundante, que es de donde venía casi todo el ahorro. Lo que el optimizador cede es el Tm identidad ocasional en mitad de un objeto de texto, unos cuantos bytes por página antes de que Flate los vea siquiera, a cambio de una garantía que la cabecera del módulo declara sin rodeos: cada transformación es output-equivalente y nunca cambia la página visible. Un optimizador de tamaño que mueve texto no es un optimizador, es un bug de renderizado con buenas tasas de compresión
El parser de content streams, el optimizador peephole y las opciones de compresión al guardar descritas aquí vienen todos en PDF Library for Delphi y C++Builder, que también expone NormalizeContentStreams, CompressContent y TPDFlibSaveOptions para afinar cómo se escribe cada documento