PDFlibPas, la losLab PDF Developer Library per Delphi, scrive ogni numero che mette in un content stream con separatore decimale a punto e senza esponente, qualunque cosa dicano le impostazioni regionali di Windows. Dalla v3.539.26 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, l'output text-to-path e il ricoloraggio formattano gli operandi attraverso PLDoubleToStrConst, e dalla v3.539.33 i parser che rileggono quei numeri usano PLTryStrToFloatInvariant invece del locale di sistema. Su una macchina tedesca, francese o brasiliana lo stesso codice ora produce gli stessi byte di una statunitense, che è l'unico comportamento che un formato di file possa tollerare
Perché un locale con decimali a virgola corrompe un PDF senza errori?
Un locale con decimali a virgola corrompe un PDF in silenzio perché la virgola non è un carattere numerico nella sintassi PDF, quindi il danno si legge come token validi con significato sbagliato. Prima della correzione, PLFloatToStr era niente più che una chiamata nuda di FloatToStr, e FloatToStr segue FormatSettings.DecimalSeparator. Con separatore a virgola, AddPageMatrix(0.5, 0.5, 0, 0) scriveva 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 ammette cifre, un punto e un segno iniziale in un numero e nient'altro, quindi un parser del contenuto legge quella riga come il numero 0 seguito da un token sconosciuto ,5, e l'operatore cm finisce con operandi sbagliati. Niente eccezioni, niente log. La pagina semplicemente si renderizza con una matrice di trasformazione che è andata alla deriva, e risalire a ritroso da un disegno spostato a un'impostazione di locale è un pomeriggio miserabile
Il secondo difetto si nasconde dietro al primo. FloatToStr usa il formato ffGeneral, che passa alla notazione a esponente appena la grandezza scende sotto 1E-4, quindi un offset minuscolo usciva come 1E-5. Lo stesso §7.3.3 dichiara che il PDF non supporta la forma a esponente, il che significa che anche una macchina con locale USA poteva scrivere un operando non valido con un valore abbastanza piccolo. I test di regression di questa release bloccano entrambe le forme di fallimento: girano il separatore a virgola, chiamano l'API e scansionano il contenuto risultante alla ricerca di qualunque token che contenga una virgola o un esponente
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 desktop de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 e successive scrivono: 0.5 0 0 0.25 0.00001 12.75 cm
// build precedenti scrivevano: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Due tipi di numeri, due famiglie di helper
La correzione in PDFlibPas è una separazione rigorosa: i numeri mostrati alle persone possono seguire il locale, i numeri scritti per una macchina mai. PLFloatToStr e PLStrToFloat restano in PDFlibExtra.pas per il testo rivolto all'utente, e la loro dichiarazione ora porta un commento che dice esattamente questo. Tutto ciò che finisce come sintassi PDF passa per PLDoubleToStrConst con un numero fisso di decimali scelto per il compito: sei per le matrici, quattro per le coordinate e le correzioni TJ, tre per i colori e i rettangoli FDF. L'audit della v3.539.26 ha toccato più call site di quanto suggerisse la segnalazione originale:
AddPageMatrix,ScalePageeDeskewPage, che tutti prepongono uncmal contenuto esistente della pagina- I costruttori di elementi di pagina che emettono reset
Tm, avanzamentiTJe trasformazionicm - Matrici di posizionamento dei glyph e punti di outline nel convertitore text-to-path
- Il box di fill nero che
RedactRegionprepone, i valori/Rectnell'export FDF e gli operandi che il ricoloraggio scrive
PLDoubleToStrConst è un formattatore scritto a mano piuttosto che un wrapper attorno a FloatToStrF, e tre delle sue proprietà contano qui. Scrive sempre un punto e toglie gli zeri finali, così 0.5 resta 0.5 invece di 0.500000. Non scrive mai un esponente per input finito. E un valore diverso da zero più piccolo della precisione richiesta conserva le sue cifre significative invece di collassare a zero, così PLDoubleToStrConst(1E-9, 6) restituisce 0.000000001; solo valori sotto circa 5E-16 diventano 0. Questa ultima regola esiste perché arrotondare a zero un fattore di scala minuscolo trasforma una matrice valida in una singolare, che è un bug peggiore di quello in corso di correzione
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Output per macchina: decimale a punto, niente esponente, zeri finali tolti
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // conserva 4 cifre significative
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Input da macchina: fallimento morbido invece di EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // i numeri del contenuto non usano mai la virgola
end;
Perché il lato parsing è più pericoloso del lato scrittura?
Il lato parsing è più pericoloso perché un parser legato al locale non produce un numero sbagliato, lancia. PLStrToFloat chiama StrToFloat, che solleva EConvertError quando il testo non corrisponde al separatore di sistema. Su un sistema a decimali con virgola questo significava che RecolorPage abortiva nell'istante in cui incontrava un banale operatore 0.5 g, quindi falliva ogni pagina reale, non solo quelle esotiche. RenderPageRegionToFile rifiutava il proprio formato di clip documentato "10.5,20.5,50.5,40.5", e gli attributi di lunghezza SVG, i colori dell'export SVG, le liste di vertici delle annotazioni e i valori di solidità degli output intent venivano rifiutati o silenziosamente sostituiti dai default. Una libreria che funziona perfettamente sulla macchina dello sviluppatore e fallisce sul primo cliente a Monaco è esattamente il tipo di codice che, come i casi in l'articolo sul codice Delphi che funziona per caso, appare corretto solo per dove è stato testato
La v3.539.33 ha classificato ogni chiamata a StrToFloat e TryStrToFloat in base a da dove arriva l'input. Gli operandi dei content stream, gli attributi SVG, le stringhe di colore dei painter e le liste di clip e vertici separate da virgole hanno tutti una sintassi a punto fissa, quindi ora passano per PLTryStrToFloatInvariant, che ripulisce il testo, lo analizza con PLInvariantFormatSettings e restituisce False per input vuoto, malformato o non finito invece di lanciare. Una lista separata da virgole non lascia spazio a compromessi, perché una virgola non può essere insieme delimitatore di lista e separatore decimale. Lo stesso passaggio ha anche corretto una scrittura fuori dai limiti: RenderPageRegionToFile memorizzava un quinto valore di clip oltre il suo buffer a quattro elementi. Per la pipeline di ricoloraggio descritta in la guida alla conversione di un PDF in un unico color space, il risultato pratico è che RecolorPage e RecolorDocument non abortiscono più su un sistema a virgola decimale. I valori delle regole che un chiamante digita in CheckDocumentPolicy sono l'unico caso di parsing che usa invece l'helper permissivo, per la ragione che spiega la sezione successiva
Che cosa succede se correggi una sola estremità di un round trip?
Correggere una sola estremità di un round trip di locale rompe codice che funzionava, ed è per questo che la modifica degli attributi di struttura della v3.539.32 ha spostato insieme lo scrittore e il lettore. I wrapper SetStructElem* passano i numeri come stringhe: SetStructElemBBox formatta quattro valori in una stringa, la memorizza attraverso AddTagAttribute, e lo scrittore /A in seguito analizza quella stringa per decidere se diventa un numero, un array o un name. Entrambe le estremità usavano il locale di sistema, quindi su un sistema a virgola decimale il round trip era autoconsistente. Il bug si mostrava solo quando un chiamante seguiva la documentazione e passava "0.5" a AddTagAttribute: il lettore non riusciva a analizzarla ed emetteva il name PDF /0.5. Il placeholder PDF/VCR aveva il problema speculare, perché la libreria generava GTS_BBox con il punto e poi lo validava con il locale prima di salvare
Cambiare solo lo scrittore al punto sarebbe stato peggio che non fare nulla, dato che ogni valore SetStructElem* avrebbe poi fallito il lettore legato al locale e sarebbe degradato in name. Quindi gli scrittori ora usano PLDoubleToStrConst(v, 6), e il lettore usa il nuovo PLTryStrToFloatLenient, che prova prima la forma a punto e ripiega sul locale di sistema. Un chiamante con locale a virgola che passava "1,25" in passato ottiene ancora il numero 1.25. Il trade-off è deliberato e documentato: su un sistema tedesco "1.500" diventava un name perché StrToFloat rifiuta i separatori delle migliaia, e ora si legge come 1.5, mentre le stringhe letterali NAN e INF non sono più accettate come numeri
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // chiamante con decimali a virgola
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, prima /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // resta /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Dove NaN e infinito vengono fermati
AddPageMatrix, ScalePage e RedactRegion ora rifiutano NaN e argomenti infiniti all'ingresso e restituiscono 0, perché nessun numero PDF può rappresentarli. ScalePage rifiutava già fattori zero o negativi, ma NaN passa un test <= 0, quindi una scala NaN arrivava fino al formattatore. Nella v3.539.26 quel formattatore chiamava ancora Round su NaN, che solleva EInvalidOp su Win32 dove l'unità x87 non maschera le operazioni non valide; la v3.539.31 ha fatto sì che PLDoubleToStrConst scriva 0 per NaN come ultima linea di difesa, ma uno zero in una matrice è una trasformazione singolare, quindi il controllo a livello API resta la vera correzione. Due confini restano in vigore di proposito. Le stringhe di stato dei metafile vengono scritte e lette con il locale dentro un solo processo e non lo lasciano mai, quindi sono state lasciate in pace. E un test che formatta 1E-5 attraverso il percorso degli elementi di pagina deve leggere il contenuto prima che il layer venga riscritto, perché riemettere gli operandi alla precisione del documento trasforma legittimamente quel valore in 0
Se la tua applicazione arriva a clienti fuori dal mondo dei decimali con punto, l'abitudine più sicura è quella che la suite di test di PDFlibPas ora usa: esegui i percorsi che producono PDF una volta con FormatSettings.DecimalSeparator impostato a virgola e scansiona l'output alla ricerca di virgole ed esponenti. L'articolo sulla conservazione della precisione decimale analizzata copre l'altra metà della stessa storia, come i numeri letti da un file esistente conservano il loro testo esatto al salvataggio. Download, il riferimento API completo e la build di prova sono sulla pagina di prodotto della PDFlibPas Delphi PDF library