PDFlibPas, la PDF Developer Library de losLab para Delphi, escribe cada número que mete en un content stream con separador decimal de punto y sin exponente, digan lo que digan las opciones regionales de Windows. Desde v3.539.26, AddPageMatrix, ScalePage, DeskewPage, RedactRegion, el output de text-to-path y el recoloreado formatean operandos por PLDoubleToStrConst, y desde v3.539.33 los parsers que leen esos números de vuelta usan PLTryStrToFloatInvariant en lugar del locale del sistema. En una máquina alemana, francesa o brasileña, el mismo código produce ahora los mismos bytes que en una de EE. UU., que es el único comportamiento que un formato de archivo puede tolerar
¿Por qué un locale con coma decimal corrompe un PDF sin dar error?
Un locale con coma decimal corrompe un PDF en silencio porque la coma no es un carácter numérico en la sintaxis PDF, así que el daño se lee como tokens válidos con significado equivocado. Antes del fix, PLFloatToStr no era más que una llamada pelada a FloatToStr, y FloatToStr sigue FormatSettings.DecimalSeparator. Con separador coma, AddPageMatrix(0.5, 0.5, 0, 0) escribía 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 admite dígitos, un punto y un signo inicial en un número y nada más, así que un parser de contenido lee esa línea como el número 0 seguido de un token desconocido ,5, y el operador cm acaba con operandos equivocados. Nada lanza, nada registra. La página sencillamente se renderiza con una matriz de transformación que se ha desviado, y remontar desde un dibujo fuera de sitio hasta una configuración regional es una tarde miserable
El segundo defecto se esconde detrás del primero. FloatToStr usa el formato ffGeneral, que salta a notación de exponente en cuanto la magnitud baja de 1E-4, así que un offset diminuto salía como 1E-5. La misma §7.3.3 afirma que PDF no soporta la forma de exponente, lo que significa que hasta una máquina con locale de EE. UU. podía escribir un operando inválido con un valor bastante pequeño. Los tests de regresión de esta versión fijan las dos formas de fallo: ponen el separador en coma, llaman a la API y rastrean el contenido resultante en busca de cualquier token que contenga una coma o un exponente
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // simula un escritorio de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 y posteriores escriben: 0.5 0 0 0.25 0.00001 12.75 cm
// builds anteriores escribían: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Dos clases de números, dos familias de helpers
El fix en PDFlibPas es una separación estricta: los números que se muestran a personas pueden seguir el locale, y los números que se escriben para una máquina jamás lo hacen. PLFloatToStr y PLStrToFloat se quedan en PDFlibExtra.pas para texto de cara al usuario, y su declaración lleva ahora un comentario que dice exactamente eso. Todo lo que acaba siendo sintaxis PDF pasa por PLDoubleToStrConst con un número fijo de decimales elegido para el trabajo: seis para matrices, cuatro para coordenadas y ajustes de TJ, tres para colores y rectángulos FDF. La auditoría de v3.539.26 tocó más sitios de llamada de los que sugería el bug report original:
AddPageMatrix,ScalePageyDeskewPage, que todos anteponen uncmal contenido existente de la página- Los constructores de elementos de página que emiten resets de
Tm, avances deTJy transformaciones decm - Matrices de colocación de glifos y puntos de contorno en el conversor de text-to-path
- La caja de relleno negro que anteponde
RedactRegion, los valores de/Recten el export FDF y los operandos que escribe el recoloreado
PLDoubleToStrConst es un formateador hecho a mano en lugar de un wrapper alrededor de FloatToStrF, y tres de sus propiedades importan aquí. Escribe siempre un punto y recorta los ceros finales, así que 0.5 se queda en 0.5 y no en 0.500000. Nunca escribe un exponente para entrada finita. Y un valor distinto de cero menor que la precisión pedida conserva sus dígitos significativos en lugar de colapsar a cero, así que PLDoubleToStrConst(1E-9, 6) devuelve 0.000000001; solo los valores por debajo de unos 5E-16 se convierten en 0. Esa última regla existe porque redondear un factor de escala diminuto a cero convierte una matriz válida en una singular, que es un bug peor que el que se estaba arreglando
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Salida de máquina: decimal con punto, sin exponente, ceros finales recortados
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // conserva 4 dígitos significativos
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Entrada de máquina: fallo suave en lugar de EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // los números de contenido nunca usan coma
end;
¿Por qué el lado de parseo es más peligroso que el de escritura?
El lado de parseo es más peligroso porque un parser atado al locale no produce un número equivocado: directamente lanza. PLStrToFloat llama a StrToFloat, que eleva EConvertError cuando el texto no casa con el separador del sistema. En un sistema con coma decimal eso significaba que RecolorPage abortaba en el momento en que se encontraba un operador 0.5 g corriente, así que fallaba cualquier página real, no solo las exóticas. RenderPageRegionToFile rechazaba su propio formato de clip documentado, "10.5,20.5,50.5,40.5", y los atributos de longitud SVG, los colores del export SVG, las listas de vértices de anotaciones y los valores de solidez de output intent se rechazaban o se sustituían en silencio por valores por defecto. Una library que funciona de lujo en la máquina del desarrollador y falla con el primer cliente de Múnich es justo el tipo de código que, como los casos del artículo sobre código Delphi que funciona por accidente, solo parece correcto por donde fue probado
v3.539.33 clasificó cada llamada a StrToFloat y TryStrToFloat según de dónde viene su entrada. Los operandos de content streams, los atributos SVG, las cadenas de color del painter y las listas de clip y vértices separadas por comas tienen todas una sintaxis fija de punto, así que ahora pasan por PLTryStrToFloatInvariant, que recorta el texto, lo parsea con PLInvariantFormatSettings y devuelve False ante entrada vacía, malformada o no finita en lugar de lanzar. Una lista separada por comas no deja sitio para el compromiso, porque una coma no puede ser a la vez delimitador de lista y marca decimal. El mismo pase arregló además una escritura fuera de límites: RenderPageRegionToFile guardaba un quinto valor de clip más allá de su buffer de cuatro elementos. Para el pipeline de recoloreado descrito en la guía para convertir un PDF a un espacio de color, el resultado práctico es que RecolorPage y RecolorDocument ya no abortan en un sistema con coma decimal. Los valores de reglas que un llamador teclea en CheckDocumentPolicy son el único caso de parseo que usa el helper tolerante en su lugar, por la razón que explica la sección siguiente
¿Qué pasa si arregla solo un extremo de un round trip?
Arreglar solo un extremo de un round trip de locale rompe código que funcionaba, y por eso el cambio de atributos de estructura de v3.539.32 movió juntos al writer y al reader. Los wrappers SetStructElem* transportan números como cadenas: SetStructElemBBox formatea cuatro valores en una cadena, la guarda por AddTagAttribute, y el writer del /A parsea después esa cadena para decidir si se convierte en número, array o name. Ambos extremos usaban el locale del sistema, así que en un sistema con coma decimal el round trip era autoconsistente. El bug solo aparecía cuando un llamador seguía la documentación y pasaba "0.5" a AddTagAttribute: el reader no podía parsearlo y emitía el PDF name /0.5. El placeholder PDF/VCR tenía el problema espejo, porque la library generaba GTS_BBox con punto y luego lo validaba con el locale antes de guardar
Cambiar solo el writer al punto habría sido peor que no hacer nada, porque cada valor de SetStructElem* fallaría entonces el reader atado al locale y se degradaría a un name. Así que los writers usan ahora PLDoubleToStrConst(v, 6), y el reader usa el nuevo PLTryStrToFloatLenient, que prueba primero la forma con punto y recurre al locale del sistema. Un llamador con locale de coma que pasaba "1,25" antes sigue obteniendo el número 1.25. El trade-off es deliberado y está documentado: en un sistema alemán "1.500" se convertía antes en un name porque StrToFloat rechaza separadores de miles, y ahora se lee como 1.5, mientras que las cadenas literales NAN e INF ya no se aceptan como números
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // llamador con coma decimal
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, antes /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // sigue siendo /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Dónde se detienen NaN e infinito
AddPageMatrix, ScalePage y RedactRegion rechazan ahora NaN y argumentos infinitos de entrada y devuelven 0, porque ningún número PDF puede representarlos. ScalePage ya se negaba a factores de cero o menos, pero NaN pasa un test de <= 0, así que un factor NaN viajaba hasta el formateador. En v3.539.26 ese formateador todavía llamaba a Round sobre NaN, que eleva EInvalidOp en Win32 donde la unidad x87 no enmascara las operaciones inválidas; v3.539.31 hizo que PLDoubleToStrConst escribiera 0 para NaN como última línea de defensa, pero un cero en una matriz es una transformación singular, así que la comprobación a nivel de API sigue siendo el fix de verdad. Dos fronteras se quedan como están a propósito. Las cadenas de estado de metafiles se escriben y leen con el locale dentro de un proceso y jamás lo abandonan, así que se dejaron tranquilas. Y un test que formatea 1E-5 por la vía de los elementos de página debe leer el contenido antes de que la capa se reescriba, porque reemitir operandos a precisión de documento convierte legítimamente ese valor en 0
Si su aplicación llega a clientes fuera del mundo del punto decimal, el hábito más seguro es el que usa ahora la suite de tests de PDFlibPas: ejecute una vez las rutas que producen PDF con FormatSettings.DecimalSeparator puesto en coma y rastree la salida en busca de comas y exponentes. El artículo sobre conservar la precisión decimal parseada cubre la otra mitad de la misma historia, cómo los números leídos de un archivo existente conservan su texto exacto al guardar. Descargas, la referencia completa de la API y la build de prueba están en la página de producto de la PDFlibPas Delphi PDF library