PDF Library for Delphi (PDFlibPas) emituje poprawny JSON dla każdej liczby PDF od v3.539.31. GetObjectJSON przepisuje tokeny, które ISO 32000-1 akceptuje, a RFC 8259 odrzuca, jak -.25, +1.5 i 007.5, na -0.25, 1.5 i 7.5 cyfra w cyfrę; GetDocumentJSON i raporty analityczne piszą null dla NaN i Infinity; a PLDoubleToStr pisze 0 dla NaN zamiast rzucać EInvalidOp w połowie eksportu. Przed poprawką biblioteka potrafiła wyprodukować JSON, którego własny czytnik odmawiał wczytać z powrotem
Dlaczego poprawna liczba PDF łamie JSON?
Bo dwie gramatyki różnią się w czterech drobnych szczegółach, a parser PDF, który szanuje tekst źródłowy, przeniesie te szczegóły prosto do wyjścia. ISO 32000-1 §7.3.3 pozwala liczbie zaczynać się od plusa, pomijać część całkowitą (.5), kończyć się gołą kropką (4.) i nieść wiodące zera (007.5). RFC 8259 §6 nie pozwala na nic z tego: opcjonalny minus, część całkowita będąca 0 albo zaczynająca się od 1 do 9 i co najmniej jedna cyfra po każdej kropce dziesiętnej. Producenci mają wolną rękę, by pisać postacie PDF, i sporo generatorów oraz ręcznie edytowanych plików z tego korzysta
Wyciek przyszedł z zamierzonej funkcji precyzji. Od v3.539.19 TPDFNumeric.Output zwraca dokładny tekst, który tokenizer sparsował dla liczb rzeczywistych, i właśnie to trzyma skalibrowaną wartość koloru w nienaruszeniu przy zapisie, jak opisano w artykule o zachowywaniu sparsowanej precyzji dziesiętnej PDF. Tokenizer już w drodze wejścia łata .5 na 0.5 i 4. na 4.0, a liczby całkowite są przepisywane z ich wartości, więc +3 wraca jako 3. Dosłownie przetrwa reszta: kropka wiodąca ze znakiem (-.25), jawny plus przy rzeczywistej (+1.5) i wiodące zera (007.5). Stary writer obiektów doklejał Output tuż po "value":, a TJSONParser.ParseNumber we własnym czytniku biblioteki stopuje na każdym z nich z komunikatem „Invalid JSON number", więc eksport się udawał, a ponowny import wywalał się z 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');
// Obiekt 12 to tablica zapisana jako [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 i późniejsze: wartości przychodzą jako -0.25, 1.5 i 7.5
// SetObjectJSON nie przyjmuje opcji, więc podaj 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 zachowuje każdą cyfrę?
PDFNumberTextToJSON przeliterowuje token na nowo, zamiast przeliczać go z Double. Funkcja w PDFlibObjectJSON czyta opcjonalny znak, zbiera cyfry przed i po pojedynczej kropce dziesiętnej, a potem stosuje wyłącznie poprawki, których żąda JSON: zrzuca plus, ścina wiodące zera zachowując jedno, dostarcza 0, gdy część całkowita jest pusta, zrzuca gołą kropkę końcową i odkłada minus z powrotem. Token zawierający jakikolwiek inny znak albo żadnych cyfr spada do PLJSONNumber(Value, 10), które pisze null, gdy wartość nie jest skończona
-.25staje się-0.25, a+.5staje się0.5+1.5staje się1.5007.5staje się7.5, a0.75zostaje takie, jakie jest4.staje się4, gdyby taki token kiedykolwiek dotarł do writera2.22221i1.250000zachowują każdą cyfrę ułamkową, łącznie z końcowymi zerami
Formatowanie z przechowywanego Double byłoby krótsze i złe, z tego samego powodu, dla którego istnieje poprawka precyzji: domyślna precyzja wyjścia to cztery miejsca dziesiętne, a nawet konwersja z pełną precyzją potrafi dodać szum binarny do literału dziesiętnego. Trzymanie cyfr oznacza, że SetObjectJSON i ImportObjectJSON, które oddają tekst każdej liczby JSON tokenizatorowi PDF, odtwarzają dokładnie tę samą wartość. Gwarancja obejmuje wartość, nie bajty: po ponownym imporcie -.25 jest przechowywane i zapisywane jako -0.25. Oba zapisy są równe w świetle §7.3.3, ale diff na poziomie bajtów oznaczy zmianę, więc nie traktuj cyklu eksport-import jako no-op na dokumencie, którego bajty są objęte podpisem
Co się dzieje z liczbą, której JSON nie umie przedstawić?
GetDocumentJSON pisze teraz null dla każdej liczby, która jest NaN albo nieskończona, bo RFC 8259 §6 nie ma składni ani na jedno, ani na drugie. Nieskończoność łatwiej wyprodukować, niż brzmi: tokenizer PDF akumuluje cyfry przez powtarzane mnożenie do Double, który kończy się w okolicach 1.8 × 10308, więc literał całkowity trochę ponad 300 cyfr po cichu staje się +Inf. Uczciwe pliki nigdy nie zawierają takiego literału; pliki furowane i wrogie zawierają, i dlatego należą do tego samego korpusu testowego co przypadki z artykułu o hartowaniu parsera PDF w Pascalu przeciw złośliwym plikom. Stary writer dokumentów formatował niecałkowite przez Str(D:0:6), a dla +Inf pisze to tekst +Inf, czego żaden konsument JSON nie sparsuje
null jest z premedytacją stratne. Konsumenci wyjścia GetDocumentJSON muszą akceptować null wszędzie tam, gdzie może pojawić się liczba, i powinni czytać go jako „wartość była, ale nie da się jej przedstawić", a nie jako brakujący klucz. Oryginalnego literału nie da się odzyskać z JSON dokumentu, więc potok, któremu zależy, powinien zalogować obiekt i traktować plik jako podejrzany, zamiast podstawiać wartość domyślną
Dlaczego pojedyncze NaN mogło przerwać eksport SVG albo JSON?
Bo PLDoubleToStr, niezmienny formatter liczb stojący za strumieniami treści, SVG, XML, CSV i większością JSON w bibliotece, skalował swoje wejście i wołał Round, a Round(NaN) podnosi EInvalidOp na celach takich jak Win32, gdzie Delphi zostawia wyjątek niepoprawnej operacji x87 odmaskowany. Wyjątek strzelał po tym, jak writer zdążył już wyemitować część wyjścia, więc jedna zdegenerowana miara, 0/0 w metryce albo NaN podany przez wywołującego, zostawiały za sobą ucięty plik. PLDoubleToStr zwraca teraz 0 dla NaN, a jego gałąź całkowita przycina do ±9.2e18 jak gałąź ułamkowa, więc Infinity również wychodzi jako skończony literał
Zero to właściwa odpowiedź dla strumienia treści, gdzie slot liczbowy musi trzymać liczbę, i niewłaściwa dla raportu, gdzie 0 to wiarygodna miara. Writery JSON, które muszą zachować tę różnicę, używają PLJSONNumber(Value, Decimals) z PDFlibExtra, które pisze null dla NaN albo Infinity, a w pozostałych przypadkach cyfry niezależne od locale. PLJSONNumber podpiera teraz GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON oraz raporty barcode, deskew, structured text i PDF/VCR; raport deskew pisał wcześniej 0 dla nieskończonego kąta, a teraz pisze null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Najpierw sformatuj każde Double do tekstu; PLJSONNumber pisze null
// dla NaN albo Infinity i zawsze używa kropki dziesiętnej
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Nigdy B.Append(Angle): przeciążenie Double stosuje locale użytkownika
Result := B.ToString;
finally
B.Free;
end;
end;
Gdzie locale użytkownika wciąż przemyca się do JSON?
Przez dowolny formatter, który zagląda do ustawień regionalnych, a pełny audyt wyjścia czytelnego dla maszyn znalazł dokładnie jeden pozostały: maxAcceptedMeanError w GetSimilarImageDeduplicationReportJSON, pisane przez PLFloatToStr, cienki wrapper nad FloatToStr. Na pulpicie z separatorem dziesiętnym przecinkiem raport zawierał "maxAcceptedMeanError":1,5, co parser JSON czyta jako wartość 1, po której następuje zbłąkany token. Pole raportuje najgorszy zaakceptowany błąd pikselowy z percepcyjnej deduplikacji obrazów i teraz przechodzi przez PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Została pułapka PLStringBuilder: na Delphi to zwykły alias System.SysUtils.TStringBuilder, którego przeciążenie Append(Double) formatuje przez locale użytkownika, podczas gdy buildy FPC używają klasy z biblioteki, więc test na Free Pascalu albo maszynie en-US nigdy go nie złapie
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Odtwórz niemiecki albo francuski pulpit wewnątrz uruchomienia testu
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Użyj fixture, który faktycznie zawiera niemal duplikaty obrazów,
// inaczej błąd średni wynosi 0 i błąd pozostaje ukryty
Lib.LoadFromFile('scanned-batch.pdf', '');
// Próba na sucho z progami 2, 2, 4: dokument nie jest modyfikowany
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;
Zestaw regresyjny dla wyjścia JSON potrzebuje trzech fixture'ów, żeby pozostać uczciwym: strony niosącej -.25, +1.5 i 007.5, obiektu trzymającego liczbę całkowitą o 400 cyfrach i dowolnego raportu uruchomionego w locale z przecinkiem, każdy walidowany ścisłym parserem, a nie obejrzanym okiem. JSON obiektów, JSON dokumentów i raporty analityczne w PDF Library for Delphi dzielą te same reguły liczb na Delphi, C++Builderze i Free Pascalu; pełna lista funkcji jest na stronie produktu PDF Library for Delphi