PDF Library for Delphi (PDFlibPas) ispušta valjani JSON za svaki PDF broj od v3.539.31. GetObjectJSON prepravlja tokene koje ISO 32000-1 prima, a RFC 8259 odbija, poput -.25, +1.5 i 007.5, u -0.25, 1.5 i 7.5 znamenku po znamenku; GetDocumentJSON i analitička izvješća zapisuju null za NaN i Infinity; a PLDoubleToStr zapisuje 0 za NaN umjesto da usred izvoza podigne EInvalidOp. Prije ispravka biblioteka je mogla proizvesti JSON koji njezin vlastiti reader odbija učitati
Zašto valjani PDF broj slomi JSON?
Jer se dvije gramatike ne slažu u četiri sitna detalja, a PDF parser koji poštuje izvorni tekst nosit će te detalje ravno u izlaz. ISO 32000-1 §7.3.3 dopušta broju da kreće plusom, izostavi cijeli dio (.5), završi na goloj točki (4.) i nosi vodeće nule (007.5). RFC 8259 §6 ne dopušta ništa od toga: neobavezni minus, cijeli dio koji je ili 0 ili kreće znamenkom 1 do 9, i barem jedna znamenka iza svake decimalne točke. Proizvođači smiju pisati PDF forme, i dosta generatora te ručno uređivanih datoteka to i čini
Curenje je došlo iz namjerne preciznosti. Od v3.539.19 TPDFNumeric.Output vraća točan tekst koji je tokenizer parsirao za realne brojeve, i to je ono što kalibriranu vrijednost boje čini točnom pri spremanju, kako opisuje očuvanje parsirane PDF decimalne preciznosti. Tokenizer već na ulazu krpi .5 u 0.5 i 4. u 4.0, a cijeli brojevi preformiliraju se iz svoje vrijednosti, pa se +3 vraća kao 3. Ono što preživi doslovno jest ostatak: točka s predznakom na početku (-.25), izričit plus na realnom broju (+1.5) i vodeće nule (007.5). Stari je object writer nadodavao Output odmah iza "value":, a TJSONParser.ParseNumber u vlastitom readeru biblioteke staje na svakom od tih s „Invalid JSON number", pa je izvoz uspio, a ponovni uvoz pao 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 jest polje zapisano kao [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 i noviji: vrijednosti stižu kao -0.25, 1.5 i 7.5
// SetObjectJSON ne prima opcije, pa predaj 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;
Kako PDFNumberTextToJSON čuva svaku znamenku?
PDFNumberTextToJSON token ponovno izgovara umjesto da ga ponovno izračuna iz Doublea. Funkcija u PDFlibObjectJSON pročita neobavezni predznak, pokupi znamenke prije i iza jedne decimalne točke, a onda primijeni samo izmjene koje JSON traži: baca plus, reže vodeće nule uz zadržavanje jedne, doda 0 kad je cijeli dio prazan, baca golu završnu točku i vrati minus. Token koji sadrži bilo koji drugi znak, ili nijednu znamenku, vraća se na PLJSONNumber(Value, 10), koja zapisuje null kad vrijednost nije konačna
-.25postaje-0.25, a+.5postaje0.5+1.5postaje1.5007.5postaje7.5, dok0.75ostaje kakav jest4.postaje4ako takav token ikad stigne do writera2.22221i1.250000zadržavaju svaku razlomkovnu znamenku, zaostale nule uključeno
Formatiranje iz pohranjenog Doublea bilo bi kraće i pogrešno, iz istog razloga iz kojeg postoji ispravak preciznosti: zadana izlazna preciznost je četiri decimale, pa čak i pretvorba punom preciznošću može dodati binarni šum decimalnom literalu. Zadržavanje znamenki znači da SetObjectJSON i ImportObjectJSON, koji svaki JSON brojni tekst predaju PDF tokenizeru, stvaraju točno istu vrijednost. Jamstvo pokriva vrijednost, ne bajtove: nakon ponovnog uvoza -.25 čuva se i sprema kao -0.25. Oba su zapisa jednaka pod §7.3.3, ali će byte-level diff označiti promjenu, pa ciklus izvoza i uvoza ne tretirajte kao no-op na dokumentu čije bajtove pokriva potpis
Što se dogodi s brojem koji JSON ne može predstaviti?
GetDocumentJSON sada zapisuje null za svaki broj koji je NaN ili beskonačan, jer RFC 8259 §6 nema sintaksu ni za jedno ni za drugo. Infinity je lakše proizvesti nego što zvuči: PDF tokenizer nakuplja znamenke ponavljanim množenjem u Double, koji vrhunac ima blizu 1.8 × 10308, pa cijelobrojni literal malo preko 300 znamenki tiho postane +Inf. Poštene datoteke nikad ne sadrže takav literal; fuzzirane i neprijateljske ga sadrže, zato pripadaju istom test korpusu kao slučajevi u otvrđivanju Pascal PDF parsera protiv zlonamjernih datoteka. Stari je document writer formatirao necijele brojeve s Str(D:0:6), a za +Inf to zapiše tekst +Inf, koji nijedan JSON konzument neće parsirati
null je namjerno gubitkiv. Konzumenti izlaza GetDocumentJSON moraju prihvatiti null bilo gdje gdje se broj može pojaviti, i trebaju ga čitati kao „vrijednost je bila prisutna ali se ne može predstaviti", a ne kao nepostojeći ključ. Izvorni literal nije oporavljiv iz JSON-a dokumenta, pa pipeline kojem je stalo neka zapiše objekt u log i tretira datoteku kao sumnjivu umjesto da nametne zadanu vrijednost
Zašto je jedan NaN mogao prekinuti SVG ili JSON izvoz?
Jer se PLDoubleToStr, invarijantni formater brojeva iza content streamova, SVG-a, XML-a, CSV-a i većine JSON-a u biblioteci, skalirao svoj ulaz i zvao Round, a Round(NaN) podiže EInvalidOp na targetima poput Win32, gdje Delphi ostavlja x87 iznimku nevaljane operacije nemaskiranom. Iznimka je prasnula nakon što je writer već ispuštao dio svog izlaza, pa je jedno degenerirano mjerenje, 0/0 u metrici ili NaN koji je uputio pozivatelj, ostavilo za sobom odrezanu datoteku. PLDoubleToStr sada vraća 0 za NaN, a njegova cijelobrojna grana steže se na ±9.2e18 kao i razlomkovna, pa i Infinity izađe kao konačan literal
Nula je pravi odgovor za content stream, gdje brojno mjesto mora držati broj, i krivi odgovor za izvješće, gdje je 0 vjerodostojno mjerenje. JSON writeri koji moraju čuvati tu razliku koriste PLJSONNumber(Value, Decimals) iz PDFlibExtra, koji za NaN ili Infinity zapisuje null, a inače invarijantne znamenke. PLJSONNumber sada stoji iza GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON te izvješća o barkodovima, deskewu, strukturiranom tekstu i PDF/VCR-u; deskew izvješće je nekada za ne-konačni kut zapisivalo 0, a sada zapisuje null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Svaki Double prvo formatiraj u tekst; PLJSONNumber zapisuje null
// za NaN ili Infinity i uvijek koristi decimalnu točku
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Nikad B.Append(Angle): Double overload slijedi korisnički lokal
Result := B.ToString;
finally
B.Free;
end;
end;
Gdje se korisnički lokal još uvijek ušulja u JSON?
Kroz bilo koji formater koji konzultira regionalne postavke, a potpuna revizija strojno čitljivog izlaza našla je točno jedan preostali: maxAcceptedMeanError u GetSimilarImageDeduplicationReportJSON, zapisivan s PLFloatToStr, tankim omotom oko FloatToStr. Na radnoj površini čiji je decimalni separator zarez, izvješće je sadržavalo "maxAcceptedMeanError":1,5, što JSON parser čita kao vrijednost 1 iza koje slijedi strani token. Polje javlja najgoru prihvaćenu pixel pogrešku iz perceptual deduplikacije slika, i sada prolazi kroz PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Preostala je zamka PLStringBuilder: na Delphiju to je puki alias od System.SysUtils.TStringBuilder, čiji Append(Double) overload formatira kroz korisnički lokal, dok FPC buildovi umjesto toga koriste klasu biblioteke, pa to test na Free Pascalu ili en-US stroju nikad ne uhvati
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Reproduciraj njemačku ili francusku radnu površinu unutar test runa
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Koristi fixture koji stvarno sadrži gotovo duplicirane slike,
// inače je srednja pogreška 0 i bug ostaje skriven
Lib.LoadFromFile('scanned-batch.pdf', '');
// Suhi run s pragovima 2, 2, 4: dokument se ne mijenja
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;
Regresijskom suiteu za JSON izlaz trebaju tri fixturea da ostane pošten: stranica koja nosi -.25, +1.5 i 007.5, objekt koji drži 400-znamenkasti cijeli broj, i bilo koje izvješće vrtjeno pod zarez-lokalom, svako validirano strogim parserom umjesto pogledom. Object JSON, document JSON i analitička izvješća u PDF Library for Delphi dijele ista pravila brojeva na Delphiju, C++Builderu i Free Pascalu; potpuni popis značajki je na stranici proizvoda PDF Library for Delphi