PDF Library for Delphi (PDFlibPas) emette JSON valido per ogni numero PDF dalla v3.539.31. GetObjectJSON riscrive i token che ISO 32000-1 accetta ma RFC 8259 rifiuta, come -.25, +1.5 e 007.5, in -0.25, 1.5 e 7.5 cifra per cifra; GetDocumentJSON e i report di analisi scrivono null per NaN e Infinity; e PLDoubleToStr scrive 0 per NaN invece di sollevare EInvalidOp a export a metà. Prima della correzione, la libreria poteva produrre JSON che il suo stesso lettore rifiutava di ricaricare
Perché un numero PDF valido rompe il JSON?
Perché le due grammatiche non sono d'accordo su quattro piccoli dettagli, e un parser PDF che rispetta il testo sorgente porta quei dettagli dritti nell'output. ISO 32000-1 §7.3.3 consente a un numero di iniziare con un segno più, omettere la parte intera (.5), finire su un punto nudo (4.) e portare zeri iniziali (007.5). RFC 8259 §6 non ne consente nessuno: un segno meno opzionale, una parte intera che è 0 o inizia con 1 a 9, e almeno una cifra dopo ogni punto decimale. I producer sono liberi di scrivere le forme PDF, e un sacco di generatori e file modificati a mano lo fanno
La fuga veniva da una funzionalità di precisione deliberata. Dalla v3.539.19, TPDFNumeric.Output restituisce il testo esatto che il tokenizer ha analizzato per i numeri reali, che è ciò che mantiene esatto un valore di colore calibrato al salvataggio, come descritto in conservare la precisione decimale PDF analizzata. Il tokenizer già corregge .5 in 0.5 e 4. in 4.0 all'ingresso, e gli interi vengono riformattati dal loro valore, così +3 torna come 3. Ciò che sopravvive verbatim è il resto: un punto iniziale con segno (-.25), un più esplicito su un reale (+1.5) e zeri iniziali (007.5). Il vecchio scrittore di oggetti appendeva Output subito dopo "value":, e TJSONParser.ParseNumber nel lettore della libreria stessa si fermava su ognuno di questi con «Invalid JSON number», così l'export riusciva e il re-import falliva con PDFLIB_ERROR_OBJECT_JSON_INVALID (105)
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
JSON: AnsiString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('legacy-drawing.pdf', '') = 0 then
raise Exception.Create('load failed');
// L'oggetto 12 è un array scritto come [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 e successive: i valori arrivano come -0.25, 1.5 e 7.5
// SetObjectJSON non accetta opzioni, quindi passa 0
if Lib.SetObjectJSON(12, JSON, 0) = 0 then
raise Exception.CreateFmt('round trip rejected, error %d',
[Lib.LastErrorCode]);
Lib.SaveToFile('legacy-drawing-roundtrip.pdf');
finally
Lib.Free;
end;
end;
Come fa PDFNumberTextToJSON a conservare ogni cifra?
PDFNumberTextToJSON rispella il token invece di ricalcolarlo da un Double. La funzione in PDFlibObjectJSON legge un segno opzionale, raccoglie le cifre prima e dopo un singolo punto decimale, e poi applica solo le modifiche che il JSON esige: toglie un più, toglie gli zeri iniziali mantenendone uno, fornisce 0 quando la parte intera è vuota, toglie un punto finale nudo e rimette il meno. Un token che contiene qualsiasi altro carattere, o nessuna cifra affatto, ripiega su PLJSONNumber(Value, 10), che scrive null quando il valore non è finito
-.25diventa-0.25, e+.5diventa0.5+1.5diventa1.5007.5diventa7.5, mentre0.75resta com'è4.diventa4se un token simile arriva mai allo scrittore2.22221e1.250000conservano ogni cifra frazionaria, zeri finali inclusi
Formattare dal Double memorizzato sarebbe stato più corto e sbagliato, per la stessa ragione per cui esiste la correzione di precisione: la precisione di output di default è quattro decimali, e persino una conversione a piena precisione può aggiungere rumore binario a un letterale decimale. Conservare le cifre significa che SetObjectJSON e ImportObjectJSON, che passano ogni testo numerico JSON al tokenizer PDF, ricreano esattamente lo stesso valore. La garanzia copre il valore, non i byte: dopo un re-import, -.25 viene memorizzato e salvato come -0.25. Entrambe le grafie sono uguali sotto §7.3.3, ma un diff a livello di byte segnalerà la modifica, quindi non trattare un ciclo export e import come un no-op su un documento i cui byte sono coperti da una firma
Che cosa accade a un numero che il JSON non può rappresentare?
GetDocumentJSON ora scrive null per qualsiasi numero che sia NaN o infinito, perché RFC 8259 §6 non ha sintassi per nessuno dei due. Infinity è più facile da produrre di quanto suoni: il tokenizer PDF accumula le cifre con moltiplicazioni ripetute in un Double, che ha un tetto vicino a 1.8 × 10308, quindi un letterale intero poco sopra le 300 cifre diventa silenziosamente +Inf. I file onesti non contengono mai un letterale simile; quelli fuzzati e ostili sì, ed è per questo che appartengono allo stesso corpus di test dei casi in hardening di un parser PDF Pascal contro file ostili. Il vecchio scrittore di documenti formattava i non interi con Str(D:0:6), e per +Inf scrive il testo +Inf, che nessun consumatore JSON analizzerà
Il null è volutamente lossy. I consumatori dell'output di GetDocumentJSON devono accettare null ovunque possa apparire un numero, e dovrebbero leggerlo come «un valore era presente ma non può essere rappresentato», non come chiave mancante. Il letterale originale non è recuperabile dal JSON del documento, quindi una pipeline che ci tiene dovrebbe loggare l'oggetto e trattare il file come sospetto invece di sostituire un default
Perché un singolo NaN poteva abortire un export SVG o JSON?
Perché PLDoubleToStr, il formattatore numerico invariante dietro ai content stream, SVG, XML, CSV e alla maggior parte del JSON della libreria, scalava il proprio input e chiamava Round, e Round(NaN) solleva EInvalidOp su target come Win32, dove Delphi lascia non mascherata l'eccezione x87 di operazione non valida. L'eccezione scattava dopo che lo scrittore aveva già emesso parte del proprio output, quindi una sola misura degenerata, uno 0/0 in una metrica o un NaN passato da un chiamante, lasciava dietro un file troncato. PLDoubleToStr ora restituisce 0 per NaN, e il suo ramo intero si limita a ±9.2e18 come il ramo frazionario, quindi anche Infinity esce come letterale finito
Zero è la risposta giusta per un content stream, dove uno slot numerico deve contenere un numero, e la risposta sbagliata per un report, dove 0 è una misura plausibile. Gli scrittori JSON che devono conservare la differenza usano PLJSONNumber(Value, Decimals) da PDFlibExtra, che scrive null per NaN o Infinity e cifre invarianti altrimenti. PLJSONNumber ora sostiene GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON e i report barcode, deskew, structured text e PDF/VCR; il report deskew in precedenza scriveva 0 per un angolo non finito e ora scrive null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Formatta ogni Double in testo prima; PLJSONNumber scrive null
// per NaN o Infinity e usa sempre un punto decimale
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Mai B.Append(Angle): l'overload Double segue il locale dell'utente
Result := B.ToString;
finally
B.Free;
end;
end;
Dove il locale dell'utente si intrufola ancora nel JSON?
Attraverso qualunque formattatore che consulta le impostazioni regionali, e un audit completo dell'output leggibile dalla macchina ne ha trovato esattamente uno rimasto: maxAcceptedMeanError in GetSimilarImageDeduplicationReportJSON, scritto con PLFloatToStr, un sottile wrapper su FloatToStr. Su un desktop il cui separatore decimale è una virgola, il report conteneva "maxAcceptedMeanError":1,5, che un parser JSON legge come il valore 1 seguito da un token vagante. Il campo riporta il peggiore errore di pixel accettato dalla deduplicazione percettiva delle immagini, e ora passa per PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Una trappola residua è PLStringBuilder: su Delphi è un semplice alias di System.SysUtils.TStringBuilder, il cui overload Append(Double) formatta attraverso il locale dell'utente, mentre le build FPC usano una classe della libreria, quindi un test su Free Pascal o su una macchina en-US non lo coglierà mai
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Riproduci un desktop tedesco o francese dentro l'esecuzione del test
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Usa una fixture che contiene davvero immagini quasi duplicate,
// altrimenti l'errore medio è 0 e il bug resta nascosto
Lib.LoadFromFile('scanned-batch.pdf', '');
// Dry run con soglie 2, 2, 4: il documento non viene modificato
Lib.GetSimilarImageDeduplicationReportJSON(2, 2, 4, Report);
Parsed := TJSONObject.ParseJSONValue(Report);
if Parsed = nil then
raise Exception.Create('report is not valid JSON on a comma locale');
Parsed.Free;
finally
Lib.Free;
end;
end;
Una suite di regression per l'output JSON ha bisogno di tre fixture per restare onesta: una pagina che porta -.25, +1.5 e 007.5, un oggetto che contiene un intero di 400 cifre, e un qualunque report eseguito sotto un locale a virgola, ognuno validato con un parser rigoroso piuttosto che a occhio. Object JSON, document JSON e i report di analisi in PDF Library for Delphi condividono le stesse regole numeriche su Delphi, C++Builder e Free Pascal; la lista completa delle funzionalità è sulla pagina di prodotto di PDF Library for Delphi