PDF Library for Delphi (PDFlibPas) vydává validní JSON pro každé PDF číslo od v3.539.31. GetObjectJSON přepisuje tokeny, které ISO 32000-1 přijímá, ale RFC 8259 odmítá — jako -.25, +1.5 a 007.5 — na -0.25, 1.5 a 7.5 číslici za číslicí; GetDocumentJSON a analytické reporty píší pro NaN a Infinity null; a PLDoubleToStr zapíše pro NaN 0 místo vyhození EInvalidOp v půlce exportu. Před opravou mohla knihovna vyrobit JSON, který její vlastní čtenář odmítl načíst zpět
Proč validní PDF číslo rozbije JSON?
Protože se obě gramatiky neshodnou ve čtyřech drobných detailech a PDF parser, který respektuje zdrojový text, ponese tyhle detaily rovnou do výstupu. ISO 32000-1 §7.3.3 dovolí číslu začít plusovým znaménkem, vynechat celou část (.5), skončit na holé tečce (4.) a nést úvodní nuly (007.5). RFC 8259 §6 nedovolí nic z toho: volitelné minus, celou část buď 0 nebo začínající 1 až 9 a aspoň jednu číslici po každé desetinné tečce. Producers si mohou PDF tvary psát svobodně a řada generátorů i ručně editovaných souborů to dělá
Únik přišel ze záměrné funkce pro přesnost. Od v3.539.19 vrací TPDFNumeric.Output pro reálná čísla přesný text, který tokenizer naparsoval — to je to, co udrží kalibrovanou barvovou hodnotu přesnou při uložení, jak popisuje zachování parsované desetinné přesnosti PDF. Tokenizer už na vstupu opravuje .5 na 0.5 a 4. na 4.0 a celá čísla se přeformátují z hodnoty, takže +3 se vrátí jako 3. Verbatim přežije zbytek: znaménkem začínající tečka (-.25), explicitní plus u reálného čísla (+1.5) a úvodní nuly (007.5). Starý objektový writer přilepil Output hned za "value": a TJSONParser.ParseNumber ve vlastním čtenáři knihovny se na každém z nich zastaví s hláškou „Invalid JSON number“, takže export uspěl a re-import selhal s 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');
// Objekt 12 je pole zapsané jako [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 a novější: hodnoty dorazí jako -0.25, 1.5 a 7.5
// SetObjectJSON nebere žádné options, takže 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;
Jak PDFNumberTextToJSON drží každou číslici?
PDFNumberTextToJSON token přepisuje písmeno za písmenem místo přepočtu z Double. Funkce v PDFlibObjectJSON přečte volitelné znaménko, posbírá číslice před a za jedinou desetinnou tečkou a pak aplikuje jen úpravy, které JSON vyžaduje: zahodí plus, ořeže úvodní nuly, jednu ponechá, dodá 0, když je celá část prázdná, zahodí holou koncovou tečku a minus vrátí na místo. Token s jakýmkoli jiným znakem, nebo bez jediné číslice, skočí na zálohu PLJSONNumber(Value, 10), která zapíše null, když hodnota není konečná
-.25se stane-0.25a+.5se stane0.5+1.5se stane1.5007.5se stane7.5, zatímco0.75zůstane, jak je4.se stane4, pokud se takový token k writerovi vůbec dostane2.22221a1.250000drží každou zlomkovou číslici, koncové nuly včetně
Formátovat z uloženého Double by bylo kratší a špatně, ze stejného důvodu, proč přesnostní oprava existuje: defaultní výstupní přesnost je čtyři desetinná místa a i plnopřesná konverze může do desetinného literálu vmíchat binární šum. Držení číslic znamená, že SetObjectJSON a ImportObjectJSON, které předávají každý JSON číselný text PDF tokenizeru, znovu vyrobí přesně tutéž hodnotu. Garance se týká hodnoty, ne bajtů: po re-importu se -.25 ukládá a zapisuje jako -0.25. Oba zápisy jsou si podle §7.3.3 rovny, ale bajtový diff změnu označí, takže cyklus export a import neberte jako no-op u dokumentu, jehož bajty kryje podpis
Co se stane s číslem, které JSON nezvládne?
GetDocumentJSON teď píše null pro každé číslo, které je NaN nebo nekonečné, protože RFC 8259 §6 nemá pro obojí žádnou syntaxi. Nekonečno se vyrobí snáz, než zní: PDF tokenizer sčítá číslice opakovaným násobením do Double, který se vyčerpá kolem 1.8 × 10308, takže celočíselný literál o něco přes 300 číslic se potichu stane +Inf. Poctivé soubory takový literál nikdy neobsahují; fuzzované a zákeřné ano, a proto patří do téhož testovacího korpusu jako případy z otužování Pascal parseru PDF proti zákeřným souborům. Starý dokumentový writer formátoval necelá čísla přes Str(D:0:6) a pro +Inf zapíše text +Inf, který neparsuje žádný JSON konzument
null je záměrně ztrátové. Konzumenti výstupu GetDocumentJSON musí null přijmout všude, kde se může objevit číslo, a číst ho jako „hodnota byla přítomná, ale nedá se reprezentovat“, ne jako chybějící klíč. Původní literál se z dokumentového JSON nedozví, takže pipeline, které na něm záleží, by měla objekt zalogovat a soubor považovat za podezřelý, místo dosazení defaultu
Proč dokázalo jediné NaN přerušit SVG nebo JSON export?
Protože PLDoubleToStr, invariantní číselný formátovač za content streamy, SVG, XML, CSV a většinou JSON v knihovně, přenásobil svůj vstup a volal Round a Round(NaN) zvedá EInvalidOp na targetech jako Win32, kde Delphi nechá výjimku x87 invalid-operation odmaskovanou. Výjimka vypukla poté, co writer už část výstupu vyslal, takže jeden degenerovaný měřený údaj, 0/0 v metrice nebo NaN podstrčený volajícím, zanechal useknutý soubor. PLDoubleToStr teď vrací pro NaN 0 a celočíselná větev se stahuje na ±9.2e18 stejně jako zlomková, takže i Infinity vyleze jako konečný literál
Nula je správná odpověď pro content stream, kde číselný slot musí držet číslo, a špatná odpověď pro report, kde je 0 věrohodný měřený údaj. JSON writery, kterým záleží na rozdílu, použijí PLJSONNumber(Value, Decimals) z PDFlibExtra, která pro NaN nebo Infinity píše null a jindy invariantní číslice. PLJSONNumber teď stojí za GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON i reporty pro barcode, deskew, structured text a PDF/VCR; deskew report dřív psal pro nekonečný úhel 0 a teď píše null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Nejdřív naformátujte každé Double na text; PLJSONNumber píše null
// pro NaN nebo Infinity a pořád používá desetinnou tečku
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Nikdy B.Append(Angle): overload pro Double se řídí user locale
Result := B.ToString;
finally
B.Free;
end;
end;
Kam se user locale do JSONu ještě vkrade?
Přes jakýkoli formátovač, který sahá po regional settings, a úplný audit strojově čitelného výstupu našel přesně jeden zbývající: maxAcceptedMeanError v GetSimilarImageDeduplicationReportJSON, psaný přes PLFloatToStr, tenký obal kolem FloatToStr. Na ploše s čárkou jako desetinným oddělovačem report obsahoval "maxAcceptedMeanError":1,5, což JSON parser přečte jako hodnotu 1 následovanou tullým tokenem. Pole hlásí nejhorší přijatou pixelovou chybu z perceptuální deduplikace obrázků a teď jde přes PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Zbývající pastí je PLStringBuilder: na Delphi je to holý alias System.SysUtils.TStringBuilder, jehož overload Append(Double) formátuje přes user locale, zatímco FPC buildy používají knihovní třídu, takže test na Free Pascalu nebo en-US stroji ho nikdy nechytí
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Reprodukujte německou či francouzskou plochu přímo v test runu
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Vezměte fixture, která doopravdy obsahuje téměř duplicitní obrázky,
// jinak je mean error 0 a bug zůstane skrytý
Lib.LoadFromFile('scanned-batch.pdf', '');
// Dry run s prahy 2, 2, 4: dokument se nezmění
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;
Regresní suite pro JSON výstup potřebuje tři fixtury, aby zůstala poctivá: stránku nesoucí -.25, +1.5 a 007.5, objekt s celým číslem dlouhým 400 číslic a libovolný report puštěný pod čárkovým locale, každou validovanou přísným parserem, ne okem. Objektový JSON, dokumentový JSON i analytické reporty v PDF Library for Delphi sdílejí stejná pravidla pro čísla napříč Delphi, C++Builder a Free Pascal; kompletní seznam funkcí je na stránce produktu PDF Library for Delphi