PDF Library for Delphi (PDFlibPas) od v3.539.31 ispisuje ispravan JSON za svaki PDF broj. GetObjectJSON tokene koje ISO 32000-1 prihvata a RFC 8259 odbija, poput -.25, +1.5 i 007.5, prepisuje u -0.25, 1.5 i 7.5 cifru po cifru; GetDocumentJSON i analitički izveštaji upisuju null za NaN i Infinity; a PLDoubleToStr upisuje 0 za NaN umesto da usred izvoza podigne EInvalidOp. Pre popravke, biblioteka je mogla da proizvede JSON koji je sopstveni čitač odbijao da učita
Zašto ispravan PDF broj lomi JSON?
Zato se dve gramatike ne slažu u četiri sitna detalja, a PDF parser koji poštuje izvorni tekst te detalje unosi pravo u izlaz. ISO 32000-1 §7.3.3 dopušta da broj počne znakom plus, izostavi celobrojni deo (.5), završi se na samoj tački (4.) i nosi vodeće nule (007.5). RFC 8259 §6 ne dopušta ništa od toga: opciono minus, celobrojni deo koji je ili 0 ili počinje cifrom od 1 do 9, i bar jedna cifra iza decimalne tačke. Proizvođači smeju da upišu PDF oblike, i sasvim mnogo generatora i ručno uređivanih fajlova to i čini
Curenje je došlo iz namerne funkcije za preciznost. Od v3.539.19, TPDFNumeric.Output vraća tačan tekst koji je tokenajzer parsirao za realne brojeve, i upravo to održava kalibrisanu boju tačnom pri čuvanju, kako je opisano u tekstu o čuvanju preciznosti parsiranih PDF decimala. Tokenajzer je već ulazeći .5 krpio u 0.5 a 4. u 4.0, a celi brojevi se reformatiraju iz vrednosti, pa +3 vraća kao 3. Doslovno preživljava ostalo: tačka sa znakom ispred (-.25), eksplicitni plus na realnom broju (+1.5) i vodeće nule (007.5). Stari pisac objekata dodavao je Output odmah iza "value":, a TJSONParser.ParseNumber u sopstvenom čitaču biblioteke staje na svakom od tih oblika sa „Invalid JSON number“, pa je izvoz uspeo a ponovni uvoz pao sa 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');
// Objekat 12 je niz upisan kao [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 i noviji: vrednosti stižu kao -0.25, 1.5 i 7.5
// SetObjectJSON ne prima opcije, pa prosledite 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 cifru?
PDFNumberTextToJSON token ponovo ispisuje umesto da ga ponovo računa iz Double-a. Funkcija u PDFlibObjectJSON-u čita opcion znak, skuplja cifre ispred i iza jedne decimalne tačke, pa zatim primenjuje samo izmene koje JSON traži: baca plus, skida vodeće nule uz čuvanje jedne, dodaje 0 kad je celobrojni deo prazan, baca gol završni zapis tačke i vraća minus. Token koji sadrži bilo koji drugi znak, ili nijednu cifru, pada na PLJSONNumber(Value, 10), koji upisuje null kad vrednost nije konačna
-.25postaje-0.25, a+.5postaje0.5+1.5postaje1.5007.5postaje7.5, dok0.75ostaje kakav jeste4.postaje4ako takav token ikada stigne do pisca2.22221i1.250000čuvaju svaku decimalnu cifru, prateće nule uključeno
Formatiranje iz sačuvanog Double-a bilo bi kraće i pogrešno, iz istog razloga zbog kojeg postoji precizna popravka: podrazumevana izlazna preciznost je četiri decimale, i čak i konverzija pune preciznosti može decimalnom literalu dodati binarni šum. Čuvanje cifara znači da SetObjectJSON i ImportObjectJSON, koji svaki tekst JSON broja predaju PDF tokenajzeru, stvaraju apsolutno istu vrednost. Garancija pokriva vrednost, ne bajtove: posle ponovnog uvoza, -.25 se čuva i zapisuje kao -0.25. Oba zapisa su jednaka po §7.3.3, ali će diff na nivou bajtova označiti promenu, pa ne tretirajte ciklus izvoz-uvoz kao no-op na dokumentu čije bajtove pokriva potpis
Šta se dešava sa brojem koji JSON ne može da predstavi?
GetDocumentJSON sada upisuje null za svaki broj koji je NaN ili beskonačan, jer RFC 8259 §6 nema sintaksu ni za jedno ni za drugo. Beskonačnost je lakše proizvesti nego što zvuči: PDF tokenajzer nakuplja cifre ponovljenim množenjem u Double, koji se napuni negde oko 1.8 × 10308, pa celobrojni literal malo preko 300 cifara tiho postane +Inf. Pošteni fajlovi nikada ne sadrže takav literal; fuzzerom mutirani i neprijateljski jesu, i zato pripadaju istom test korpusu kao slučajevi iz teksta o ojačavanju Pascal PDF parsera protiv zlonamernih fajlova. Stari pisac dokumenata formatirao je ne-celi brojeve sa Str(D:0:6), a za +Inf to upisuje tekst +Inf, koji nijedan JSON potrošač neće parsirati
null namerno gubi informaciju. Potrošači izlaza GetDocumentJSON-a moraju da prihvate null bilo gde gde se broj može pojaviti, i treba da ga čitaju kao „vrednost je postojala ali se ne može predstaviti“, a ne kao nedostajući ključ. Originalni literal se iz dokumentnog JSON-a ne može povratiti, pa cevovod kojem je to stalo neka objekat zabeleži u log i fajl proglasi sumnjivim umesto da zameni podrazumevanu vrednost
Zašto je jedan jedini NaN mogao prekinuti SVG ili JSON izvoz?
Zato što je PLDoubleToStr, invarijantni formater brojeva iza content streamova, SVG-a, XML-a, CSV-a i najvećeg dela JSON-a u biblioteci, skalirao ulaz i zvao Round, a Round(NaN) podiže EInvalidOp na platformama poput Win32 gde Delphi ostavlja x87 izuzetak neispravne operacije neomaskiran. Izuzetak se aktivirao posle toga što je pisac već izbacio deo izlaza, pa je jedan degeneran izmereni podatak, 0/0 u metrici ili NaN koji je prosledio pozivalac, za sobom ostavio odsečen fajl. PLDoubleToStr sada vraća 0 za NaN, a njegova celobrojna grana seče na ±9.2e18 kao i razlomljena, pa i Infinity izlazi kao konačan literal
Nula je pravi odgovor za content stream, gde mesto za broj mora da drži broj, i pogrešan odgovor za izveštaj, gde je 0 verovatno merenje. JSON pisci koji moraju da čuvaju tu razliku koriste PLJSONNumber(Value, Decimals) iz PDFlibExtra-a, koji za NaN ili Infinity upisuje null a inače invarijantne cifre. PLJSONNumber sada stoji iza GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON i izveštaja za barkod, deskew, strukturirani tekst i PDF/VCR; deskew izveštaj je ranije za ne-konačan ugao upisivao 0 a sada upisuje 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 formatirajte u tekst; PLJSONNumber upisuje null
// za NaN ili Infinity i uvek koristi decimalnu tačku
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Nikada B.Append(Angle): Double overload prati korisnički lokal
Result := B.ToString;
finally
B.Free;
end;
end;
Gde se korisnički lokal još uvek šunja u JSON?
Kroz bilo koji formater koji pita regionalna podešavanja, i potpuna revizija mašinski čitljivog izlaza našla je tačno jedan preostali: maxAcceptedMeanError u GetSimilarImageDeduplicationReportJSON-u, upisivan kroz PLFloatToStr, tanak omotač oko FloatToStr-a. Na radnoj površini čiji je decimalni separator zarez, izveštaj je sadržao "maxAcceptedMeanError":1,5, što JSON parser čita kao vrednost 1 praćenu zalutalim tokenom. Polje prijavljuje najgoru prihvaćenu piksel grešku iz perceptual deduplikacije slika, i sada prolazi kroz PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Preostala zamka je PLStringBuilder: na Delphi-ju je to običan alias za System.SysUtils.TStringBuilder, čiji Append(Double) overload formatira kroz korisnički lokal, dok FPC buildovi koriste bibliotečku klasu, pa test na Free Pascalu ili en-US mašini nikada neće uhvatiti problem
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Reprodukujte nemačku ili francusku radnu površinu unutar testa
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Uzmite fiksaturu koja zaista sadrži skoro duplikate slika,
// inače je srednja greška 0 i bug ostaje skriven
Lib.LoadFromFile('scanned-batch.pdf', '');
// Suvi probni rad sa pragovima 2, 2, 4: dokument se ne menja
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;
Regresioni skup za JSON izlaz treba tri fiksature da ostane pošten: stranica koja nosi -.25, +1.5 i 007.5, objekat sa celobrojnim literalom od 400 cifara, i bilo koji izveštaj pokrenut uz zarez kao lokal, svaka validirana strogim parserom a ne po očima. Object JSON, document JSON i analitički izveštaji u PDF Library for Delphi dele ista pravila za brojeve na Delphi-ju, C++Builder-u i Free Pascalu; kompletna lista funkcija je na stranici proizvoda PDF Library for Delphi