PDF Library for Delphi(PDFlibPas)從 v3.539.31 起為每個 PDF 數字輸出合法 JSON。GetObjectJSON 把 ISO 32000-1 收下、RFC 8259 卻拒收的 token——像 -.25、+1.5 與 007.5——逐位改寫成 -0.25、1.5 與 7.5;GetDocumentJSON 與各分析報告對 NaN 與 Infinity 寫 null;PLDoubleToStr 對 NaN 寫 0,而不是在匯出半路丟 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 對實數回傳 tokenizer 解析出的原文,這正是保留 PDF 小數解析精度那篇講的、讓校準過的顏色值存檔不走樣的機制。tokenizer 在讀入時就已經把 .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 是重新拼寫 token,而不是從 Double 重新計算。PDFlibObjectJSON 裡的這個函式讀一個可選正負號、收集單一小數點前後的位數,然後只做 JSON 要求的修改:摘掉加號、去前導零但保留一個、整數部分為空時補 0、去掉光桿尾點、再把減號放回去。含任何其他字元、或一位數都沒有的 token,退回 PLJSONNumber(Value, 10),它在值非有限時寫 null
-.25變成-0.25,+.5變成0.5+1.5變成1.5007.5變成7.5,0.75則原樣不動4.變成4——如果這種 token 真的到得了寫出器2.22221與1.250000保住每一位小數,含尾隨零
從儲存的 Double 重新格式化會更短、但錯,理由與精度修正存在的原因相同:預設輸出精度是四位小數,就算是全精度轉換也可能給十進位字面值摻進二進位雜訊。保住位數,意味著 SetObjectJSON 與 ImportObjectJSON——它們把每段 JSON 數字文字交給 PDF tokenizer——會重建出一模一樣的值。這個保證覆蓋值,不覆蓋位元組:重匯入之後,-.25 以 -0.25 的形態儲存與存檔。兩種拼寫在 §7.3.3 之下相等,但位元組級 diff 會標出這個變化,所以別把匯出匯入循環當成文件上的 no-op——如果它的位元組被簽章覆蓋著
JSON 表示不了的數字會怎樣?
GetDocumentJSON 現在對任何 NaN 或無限大的數字寫 null,因為 RFC 8259 §6 對兩者都沒有語法。Infinity 比聽起來容易製造:PDF tokenizer 用反覆乘法把位數累進 Double,上限約 1.8 × 10308,所以一個 300 位出頭的整數字面值會無聲變成 +Inf。誠實的檔案從不含這種字面值;fuzz 出來的與惡意的檔案會有,所以它們理應與強化 Pascal PDF 解析器以抵禦惡意檔案裡的案例住進同一個測試語料。舊文件寫出器用 Str(D:0:6) 格式化非整數,對 +Inf 這會寫出文字 +Inf,沒有任何 JSON 消費者解析得了
這個 null 是刻意有損的。GetDocumentJSON 輸出的消費者必須在數字可能出現的任何地方接受 null,並把它讀成「值存在但無法表示」,而不是「鍵缺失」。原始字面值無法從文件 JSON 恢復,所以在乎這件事的管線應該記下物件、把檔案視為可疑,而不是拿預設值頂替
為什麼一個 NaN 就能中斷 SVG 或 JSON 匯出?
因為內容串流、SVG、XML、CSV 與庫裡大部分 JSON 背後的不變式數字格式化器 PLDoubleToStr 會縮放輸入再呼叫 Round,而 Round(NaN) 在 Win32 這類目標上丟 EInvalidOp——Delphi 在那些目標上讓 x87 的無效運算例外保持未遮罩。例外觸發時寫出器已經吐出了部分輸出,於是一個退化的測量值——指標裡的 0/0、或呼叫端傳進來的 NaN——就留下半份截斷檔案。PLDoubleToStr 現在對 NaN 回傳 0,整數分支也像小數分支一樣夾到 ±9.2e18,Infinity 於是同樣輸出成有限的字面值
對內容串流來說,零是正確答案——數字槽位必須放數字;對報告來說,零是錯誤答案——0 是一個看似合理的測量值。必須守住這個區分的 JSON 寫出器用 PDFlibExtra 的 PLJSONNumber(Value, Decimals):NaN 或 Infinity 寫 null,其餘情況寫不變式位數。PLJSONNumber 現在支撐 GetSimilarImageDeduplicationReportJSON、GetAnnotationHitsJSON 與條碼、deskew、結構化文字、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?
從任何會看地區設定的格式化器溜進來,而對機器可讀輸出的全面盤點只找到一個漏網的:GetSimilarImageDeduplicationReportJSON 裡的 maxAcceptedMeanError,用 PLFloatToStr 寫出——那是 FloatToStr 的薄包裝。在小數分隔符是逗號的桌面上,報告裡出現 "maxAcceptedMeanError":1,5,JSON 解析器把它讀成值 1 加一個游離 token。這個欄位回報的是感知式影像去重裡最壞的可接受像素誤差,現在改走 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,bug 繼續藏著
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 位整數的物件,以及任何一份在逗號地區下執行的報告,每個都用嚴格解析器驗證而不是用眼睛掃。PDF Library for Delphi 的物件 JSON、文件 JSON 與分析報告,在 Delphi、C++Builder 與 Free Pascal 上共用同一套數字規則;完整功能清單在PDF Library for Delphi 產品頁