Cargue un PDF calibrado, no cambie nada, guárdelo con otro nombre y compare el diccionario del espacio de color: entró /Gamma [2.22221 2.22221 2.22221], salió /Gamma [2.2222 2.2222 2.2222]. El escritor reformateó un número que nunca tuvo que tocar, y la página ahora se renderiza con colores ligeramente distintos. La cura es tratar cada decimal parseado como dos cosas a la vez, un valor para la aritmética y un literal para la salida, y seguir emitiendo el literal hasta que alguien asigne de verdad un valor nuevo
Por qué un save sin cambios puede mover los colores CalRGB
Los píxeles se mueven porque al renderer le entregan datos de calibración ligeramente distintos mientras los bytes de la imagen siguen idénticos. ISO 32000-1 §8.6.5.3 define un espacio de color CalRGB por un /WhitePoint obligatorio, un array /Gamma opcional de tres elementos y una /Matrix opcional de nueve elementos, y todos son objetos numéricos comunes en el diccionario del espacio de color. Páselos por un Double y un formateador fijo a cuatro decimales, y 2.22221 se vuelve 2.2222 mientras una entrada de matriz de 0.71519 se vuelve 0.7152. Un valor de gamma es un exponente aplicado a cada componente de cada píxel que pasa por ese espacio, así que el cambio aparece dondequiera que se use el espacio
Para un archivo que declara un espacio CalRGB para una imagen de encabezado reutilizada en 35 páginas, los 35 encabezados se renderizan con colores corridos y nada más cambia en esas páginas. Los chequeos a los que la gente recurre primero salen limpios. Un hash de los bytes decodificados de los streams compara datos de imagen, y los datos de imagen sí son idénticos; el daño está en un diccionario. El diff de revisiones descrito en el artículo sobre niveles de modificación toma la huella de un cuerpo de objeto normalizado, así que ahí las dos revisiones también hashean al mismo valor. Lo que lo atrapa es una comparación de páginas renderizadas. Para cualquier pipeline que reescriba PDFs producidos por otro software, pondríamos una comparación de renderizado por página por delante de todo chequeo estructural
Cómo puede verse un número real de PDF
ISO 32000-1 §7.3.3 define un número real como una cadena decimal: un signo opcional, dígitos, un punto, más dígitos. Sin notación de radix. Sin forma exponencial. El Anexo C, que lista los límites de implementación, espera que se respeten aproximadamente cinco dígitos decimales significativos en la parte fraccionaria. Una precisión de salida por defecto de cuatro ya queda, por lo tanto, un dígito por debajo de lo que la especificación les dice a los lectores que conserven, y el archivo no hizo nada malo al usar cinco
Cerca de cero, un formateador ingenuo hace algo peor que perder un dígito. La forma común de imprimir un Double a N decimales es multiplicar por 10^N, redondear a entero y escribir los dígitos, y cuando ese entero es cero la salida es 0. Entréguele el primer operando de 0.00001 0 0 1 0 0 cm a cuatro decimales y el resultado es 0 0 0 1 0 0 cm, una matriz singular que hace desaparecer todo lo dibujado bajo ella. Una entrada de /Matrix de -0.000012345 corre la misma suerte
Cuando PDF Library for Delphi (PDFlibPas) tiene que formatear un Double por sí misma, conserva la cantidad pedida de dígitos significativos para un valor distinto de cero menor que el paso de precisión en lugar de escribir 0. A cuatro decimales, 0.00001 se escribe como 0.00001 y 0.0000123 como 0.0000123; solo las magnitudes por debajo de aproximadamente 5E-16 siguen saliendo como 0. Eso mantiene sanos los números recién escritos. Un número parseado que nunca necesitó reformateo es otro asunto, y la biblioteca lo maneja no reformateándolo en absoluto
Cómo PDFlibPas conserva el literal que parseó
TPDFStructure.Decode conserva el texto fuente de cada real estándar que tokeniza, es decir, un token con punto decimal y sin marcador de exponente, justo al lado del Double convertido. TPDFNumeric.Output escribe ese texto de vuelta siempre que esté presente y formatea el Double al PDFPrecNum de la estructura solo cuando no lo está. TPDFNumeric.SetTo descarta el texto, porque un número editado es un número nuevo y debe seguir la precisión de salida actual
Atar el reinicio a SetTo fue deliberado. El atajo tentador es emitir el texto original a menos que el flag Changed del objeto esté activo, ya que ese flag ya existe en cada objeto. Sin embargo, el pipeline de guardado limpia Changed una vez que un objeto se escribió, así que un valor editado, guardado y editado de nuevo en la misma sesión volvería a emitir su literal viejo en el segundo guardado. Limpiar el texto dentro de la propia asignación significa que el literal y el valor nunca pueden discrepar
Algunos límites son intencionales. Los enteros no se guardan como texto porque el formateo de enteros ya es sin pérdida. Las formas exponenciales como 6.02E23 se aceptan a la entrada por consideración a los productores que de todos modos las escriben, pero a la salida se formatean como cualquier número generado por la biblioteca, ya que copiarlas de vuelta perpetuaría una sintaxis que §7.3.3 excluye. El tokenizer además aplica su pequeña reparación de siempre antes de guardar el texto: un literal con punto inicial como .5 se conserva como 0.5, un literal con punto final como 5. como 5.0. Todo lector trata cada par como el mismo número, y la grafía reparada se acepta mucho más ampliamente
El fragmento de abajo pasa los valores CalRGB directamente por la capa de objetos, así que cada regla se puede ver sin abrir un archivo PDF. Las líneas Assert son chequeos que sugerimos agregar en sus propias pruebas; no son llamadas a la biblioteca
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// Su propio chequeo: la entrada sin editar sobrevive tal cual, incluido el
// valor que un formateador ingenuo a cuatro decimales colapsaría a 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Una edición descarta el texto original y sigue a PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Los valores nuevos diminutos conservan dígitos significativos en lugar de volverse 0
Assert(Structure.NewNumeric(0.0000123).Output = '0.0000123');
// Bajar la precisión después no llega a la entrada sin editar
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Una trampa al escribir aserciones como estas: el Round de Delphi usa redondeo bancario, y 0.000012345 * 1e8 se evalúa en binario como 1234.4999… y no como 1234.5. Nos topamos de frente con ese valor al verificar la ruta de formateo cerca de cero. Elija entradas de prueba que no sean puntos medios binarios siempre que una aserción dependa de cómo redondea el último dígito
¿Qué controla SetPrecision?
TPDFlib.SetPrecision fija los decimales de los números que la biblioteca escribe: los gráficos dibujados a través del painter, los números creados a partir de un Double, por ejemplo con NewNumeric, y los valores parseados que después se cambiaron con SetTo. Acepta de 2 a 8 y devuelve 0 para cualquier cosa fuera de ese rango. Un decimal parseado que nadie editó conserva su texto de entrada sea cual sea el ajuste, y cambiar el ajuste después de LoadFromFile no llega retroactivamente a él. El código fuente pasado a SetObjectFromString pasa por el mismo tokenizer y se conserva de la misma manera
Nuestra recomendación es dejar SetPrecision en paz a menos que la geometría que genera necesite pasos más finos. Subirlo para proteger archivos de entrada no compra nada, porque la entrada sin tocar ya conserva sus propios dígitos, y alarga cada número que usted dibuja. Cinco decimales es una elección razonable para trabajo de dibujo por debajo del punto; ocho sobre todo infla los content streams
uses
PDFlibrary;
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('calibrated.pdf', '') <> 1 then
Exit;
// Gobierna los números escritos de aquí en adelante; los arrays /Gamma,
// /Matrix y /WhitePoint parseados conservan su texto original
PDF.SetPrecision(5);
PDF.SelectPage(1);
PDF.SetLineWidth(0.123456); // los números dibujados usan cinco decimales
PDF.DrawLine(72, 72, 300, 72);
PDF.SaveToFile('calibrated-out.pdf');
finally
PDF.Free;
end;
end;
Content streams: las ediciones conservan operandos, la normalización es opt-in
El modelo de contenido editable conserva el texto de los operandos por defecto y canoniza los números solo cuando la salida canónica es el objetivo de la llamada. TPDFContentProgram.CanonicalNumbers tiene por defecto False, así que una capa devuelta por ParsePageContentLayer, el mismo modelo sobre el que está construido el tracker de estado gráfico, vuelve a emitir cada operando numérico sin tocar exactamente como estaba escrito. Las ediciones de página que reescriben una capa completa, incluidas ScalePage, TransformPageElement, DeletePageElement, RedactRegion, ConvertTextToPaths y su propio programa escrito de vuelta con WritePageContentLayer, cambian solo los operandos que la edición tocó
El otro valor por defecto se ve más prolijo: canonizar en cada parse, para que la ruta de edición y NormalizeContentStreams produzcan siempre la misma grafía. Creemos que es el valor por defecto equivocado para editar. Una llamada a ScalePage sobre una capa que contiene 0.123456 w escribiría de vuelta 0.1235 w, y cada otro operando de la capa también se reescribiría, aunque quien llama pidió solo escalar. La forma canónica pertenece a las operaciones cuyo propósito es una salida comparable
Esas operaciones igual la obtienen. NormalizeContentStreams y el optimizador de contenido al guardar parsean con CanonicalNumbers en True, porque su trabajo es una salida estable a partir de una entrada arbitraria; si 0.50000 0 0 RG y 0.5 0 0 RG se emitieran distinto, toda comparación aguas abajo se iría a la deriva según las costumbres de formateo del productor. La canonización recursa por arrays y diccionarios, así que los patrones de guiones, los arrays TJ y los diccionarios de propiedades de contenido marcado quedan cubiertos, mientras que los datos crudos de imágenes inline se dejan en paz. Los operandos entregados a través de SetOperand se canonizan diga lo que diga el flag. Un programa de contenido lleva su propio almacén de números fijo a cuatro decimales, independiente de SetPrecision, así que un operando editado sale con cuatro. Solo Parse lee el flag, lo que significa que debe activarse antes de parsear; para un programa que ParsePageContentLayer ya parseó, active el flag y llame a Parse(Emit)
uses
PDFlibrary, PDFlibContentModel, PDFlibContentOps;
var
PDF: TPDFlib;
Prog: TPDFContentProgram;
I: Integer;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('calibrated.pdf', '') <> 1 then
Exit;
PDF.SelectPage(1);
Prog := PDF.ParsePageContentLayer(1);
if Prog = nil then
Exit;
try
// Cambie un ancho de línea; todos los demás operandos conservan su texto fuente
I := Prog.FindNextOp(0, coLineWidth);
if I >= 0 then
Prog.SetOperand(I, 0, '0.123456'); // emitido como 0.1235
PDF.WritePageContentLayer(1, Prog);
// Copia canónica solo para comparar, no se escribe de vuelta:
// Parse es el único que lee el flag, así que vuelva a parsear tras activarlo
Prog.CanonicalNumbers := True;
Prog.Parse(Prog.Emit);
// Prog.Emit ahora escribe 0.50000 y 0.5 de la misma manera
finally
Prog.Free;
end;
PDF.SaveToFile('calibrated-out.pdf');
finally
PDF.Free;
end;
end;
En conjunto, la regla para quien llama es corta. Un LoadFromFile común seguido de SaveToFile deja los content streams sin tocar y los números de diccionario sin tocar tal como estaban. Una página editada a través del modelo de contenido conserva el texto de cada operando que la edición no cambió. NormalizeContentStreams, el optimizador que corre cuando el contenido se comprime al guardar, y cualquier programa parseado con CanonicalNumbers en True producen números canónicos por diseño, y el resto del documento igual se conserva
Qué cuesta y dónde termina la garantía
Cada TPDFNumeric lleva una referencia AnsiString extra, y cada decimal parseado mantiene vivo su texto fuente mientras viva el objeto. En un documento con millones de números reales eso es memoria medible, y corresponde incluirlo en cualquier benchmark de documentos grandes que corra. La garantía además está acotada al propio documento del número. Copiar objetos entre documentos, o reconstruir valores a través de la API de objetos, produce números nuevos, y los números nuevos siguen la precisión de salida
Los números preservados y la salida idéntica byte a byte son propiedades distintas. Un load-and-save de un documento sin tocar conserva los números de calibración que el renderer consume, pero la identidad de bytes depende también de la numeración de objetos, la compresión de streams y el identificador del trailer que se cubre en el artículo sobre el PDF ID determinista. El diff de huellas sigue hasheando el cuerpo normalizado, lo que significa que las diferencias de redondeo en archivos escritos por otras herramientas quedan fuera de lo que compara; una comparación de renderizado sigue siendo el chequeo para esas
El manejo numérico y la semántica exacta de SetPrecision se cubren en la referencia de la losLab PDF Developer Library, junto con las llamadas del modelo de contenido usadas arriba