PDF Library for Delphi (PDFlibPas) видає коректний JSON для кожного числа PDF, починаючи з v3.539.31. GetObjectJSON переписує токени, які ISO 32000-1 приймає, а RFC 8259 відхиляє, як-от -.25, +1.5 і 007.5, у -0.25, 1.5 і 7.5 цифра-за-цифрою; GetDocumentJSON і аналітичні звіти пишуть null для NaN і Infinity; а PLDoubleToStr пише 0 для NaN замість підняття EInvalidOp посередині експорту. До виправлення бібліотека могла породити JSON, який її власний читач відмовлявся завантажити назад
Чому коректне число PDF ламає JSON?
Бо дві граматики розходяться в чотирьох дрібницях, і PDF-парсер, що поважає вихідний текст, проносить ті дрібниці прямо у вивід. ISO 32000-1 §7.3.3 дозволяє числу починатися знаком плюс, пропускати цілу частину (.5), закінчуватися голою крапкою (4.) і нести початкові нулі (007.5). RFC 8259 §6 не дозволяє жодного з цього: необов'язковий мінус, ціла частина, що або 0, або починається з 1 до 9, і щонайменше одна цифра після кожної десяткової крапки. Продюсери вільні писати PDF-форми, і чимало генераторів та файлів, правлених руками, так і роблять
Протікання прийшло з навмисної фічі точності. З v3.539.19 TPDFNumeric.Output повертає точний текст, який токенізатор розібрав для дійсних чисел, — саме те, що тримає відкаліброване колірне значення точним при збереженні, як описано в збереженні розібраної десяткової точності PDF. Токенізатор уже латав .5 у 0.5 і 4. у 4.0 на вході, а цілі переформантовуються зі свого значення, тож +3 повертається як 3. Дослівно виживає решта: знак із початковою крапкою (-.25), явний плюс у дійсного (+1.5) і початкові нулі (007.5). Старий писар об'єктів приклеював Output просто після "value":, а TJSONParser.ParseNumber у власному читачі бібліотеки зупиняється на кожному з них із «Invalid JSON number», тож експорт мав успіх, а повторний імпорт падав із 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');
// Об'єкт 12 — масив, записаний як [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 і пізніші: значення прибувають як -0.25, 1.5 і 7.5
// SetObjectJSON не приймає опцій, тож передаємо 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;
Як PDFNumberTextToJSON тримає кожну цифру?
PDFNumberTextToJSON переписує токен інакше, ніж перераховує його з Double. Функція в PDFlibObjectJSON читає необов'язковий знак, збирає цифри до й після однієї десяткової крапки, а потім застосовує лише правки, яких вимагає JSON: скидає плюс, зрізає початкові нулі, лишаючи один, постачає 0, коли ціла частина порожня, скидає голу кінцеву крапку і повертає мінус на місце. Токен із будь-яким іншим символом чи взагалі без цифр відкатується до PLJSONNumber(Value, 10), яка пише null, коли значення не скінченне
-.25стає-0.25, а+.5стає0.5+1.5стає1.5007.5стає7.5, тоді як0.75лишається як є4.стає4, якщо такий токен коли-небудь дістанеться писаря2.22221і1.250000тримають кожну дробову цифру, хвостові нулі включно
Формантування зі збереженого Double було б коротшим і неправильним, з тієї ж причини, з якої існує виправлення точності: усталена точність виводу — чотири десяткові, і навіть конвертація повної точності може додати бінарний шум до десяткового літерала. Тримати цифри означає, що SetObjectJSON і ImportObjectJSON, які віддають текст кожного JSON-числа PDF-токенізаторові, відтворюють рівно те саме значення. Гарантія покриває значення, а не байти: після повторного імпорту -.25 зберігається і записується як -0.25. Обидва написи рівні за §7.3.3, але байтовий diff позначить зміну, тож не вважайте цикл експорт-імпорт no-op на документі, чиї байти покриті підписом
Що станеться з числом, яке JSON не може зобразити?
GetDocumentJSON тепер пише null для будь-якого числа, що є NaN чи нескінченним, бо RFC 8259 §6 не має синтаксису ані для того, ані для іншого. Нескінченність легше отримати, ніж здається: PDF-токенізатор накопичує цифри повторним множенням у Double, який вичерпується близько 1.8 × 10308, тож цілий літерал трохи довший за 300 цифр тихо стає +Inf. Чесні файли ніколи не містять такого літерала; фаззинговані та ворожі — містять, саме тому їм місце в тому самому тестовому корпусі, що й випадкам з загартування Pascal-парсера PDF проти зловмисних файлів. Старий писар документів формантував нецілі через Str(D:0:6), а для +Inf це пише текст +Inf, який жоден JSON-споживач не розбере
null тут — навмисно з втратами. Споживачі виводу GetDocumentJSON мусять приймати null всюди, де може з'явитися число, і читати його як «значення було, але його неможливо зобразити», а не як відсутній ключ. Початковий літерал не відновити з JSON документа, тож конвеєр, якому не байдуже, мусить логувати об'єкт і вважати файл підозрілим, а не підставляти усталене
Чому один NaN міг обірвати SVG- чи JSON-експорт?
Бо PLDoubleToStr, інваріантний формантер чисел, що стоїть за потоками вмісту, SVG, XML, CSV і більшістю JSON у бібліотеці, масштабував свій ввід і викликав Round, а Round(NaN) підіймає EInvalidOp на цілях на кшталт Win32, де Delphi лишає x87-виняток некоректної операції незамаскованим. Виняток стріляв після того, як писар уже видав частину свого виводу, тож одне вироджене вимірювання, 0/0 у метриці чи NaN, переданий викликачем, лишало після себе обрізаний файл. PLDoubleToStr тепер повертає 0 для NaN, а його цілочисельна гілка обрізається до ±9.2e18, як і дробова, тож Infinity теж виходить скінченним літералом
Нуль — правильна відповідь для потоку вмісту, де числовий слот мусить тримати число, і неправильна для звіту, де 0 — правдоподібне вимірювання. JSON-писарі, яким треба тримати різницю, користуються PLJSONNumber(Value, Decimals) з PDFlibExtra, яка пише null для NaN чи Infinity і інваріантні цифри інакше. PLJSONNumber тепер стоїть за GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON і звітами barcode, deskew, structured text і PDF/VCR; звіт deskew раніше писав 0 для нескінченного кута, а тепер пише null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Формантуйте кожен Double у текст спершу; PLJSONNumber пише null
// для NaN чи Infinity і завжди вживає десяткову крапку
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Ніколи B.Append(Angle): перевантаження Double слідує локалі користувача
Result := B.ToString;
finally
B.Free;
end;
end;
Де локаль користувача все ще просочується в JSON?
Через будь-який формантер, що заглядає в регіональні налаштування, і повний аудит машиночитаного виводу знайшов рівно один лишок: maxAcceptedMeanError у GetSimilarImageDeduplicationReportJSON, який писався через PLFloatToStr, тонку обгортку над FloatToStr. На робочому столі, чий десятковий розділювач — кома, звіт містив "maxAcceptedMeanError":1,5, що JSON-парсер читає як значення 1 і блукаючий токен після нього. Поле звітує про найгіршу прийняту піксельну помилку з перцептуальної дедуплікації зображень, і тепер воно проходить через PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Решта пастка — PLStringBuilder: на Delphi це простий аліас System.SysUtils.TStringBuilder, чий перевантажений Append(Double) формантує через локаль користувача, тоді як FPC-збірки користуються класом бібліотеки, тож тест на Free Pascal чи машині en-US ніколи його не зловить
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Відтворюємо німецький чи французький робочий стіл у тестовому прогоні
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Візьміть фікстуру, що справді містить майже-дублікати зображень,
// інакше середня помилка 0 і баг лишається схованим
Lib.LoadFromFile('scanned-batch.pdf', '');
// Сухий прогін із порогами 2, 2, 4: документ не змінюється
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;
Регресному набору для JSON-виводу потрібно три фікстури, щоб лишатися чесним: сторінка з -.25, +1.5 і 007.5, об'єкт із 400-цифровим цілим і будь-який звіт під комовою локаллю, кожна перевірена строгим парсером, а не на око. Об'єктний JSON, JSON документа й аналітичні звіти в PDF Library for Delphi діляться тими самими правилами чисел через Delphi, C++Builder і Free Pascal; повний перелік можливостей — на сторінці продукту PDF Library for Delphi