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 会在每一个这种 token 上以「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 出发格式化会更短、但更错,理由和精度修复存在的原因相同:默认输出精度是四位小数,连全精度转换都可能给十进制字面量加进二进制噪声。留住数字意味着把每个 JSON 数字文本交给 PDF tokenizer 的 SetObjectJSON 和 ImportObjectJSON 能重建出完全相同的值。保证覆盖的是值、不是字节:重新导入后,-.25 以 -0.25 存储和保存。两种拼写在 §7.3.3 下相等,但字节级 diff 会标出这个变化,所以别在字节被签名覆盖的文档上把导出加导入的循环当成 no-op
JSON 表示不了的数字会怎样?
GetDocumentJSON 现在为任何 NaN 或无穷的数字写 null,因为 RFC 8259 §6 对两者都没有语法。Infinity 比听起来容易造出来:PDF tokenizer 靠反复乘 10 把数位累进一个 Double,后者在 1.8 × 10308 附近封顶,所以一个 300 位出头的整数字面量会无声变成 +Inf。正经文件从不含这种字面量,fuzz 出的和恶意的文件会有,所以它们理应和加固 Pascal PDF 解析器以抵御恶意文件里的用例待在同一个测试语料里。旧的文档写出器用 Str(D:0:6) 格式化非整数,对 +Inf 它写出文本 +Inf,没有哪个 JSON 消费者解析得了
这个 null 是刻意有损的。GetDocumentJSON 输出的消费者必须接受数字能出现的任何位置上的 null,并且应把它读作「有一个值、但无法表示」,而不是缺键。原始字面量无法从文档 JSON 里恢复,所以在乎这一点的管线应当记下对象、把文件当可疑处理,而不是塞一个默认值进去
为什么单个 NaN 能弄断 SVG 或 JSON 导出?
因为 PLDoubleToStr——内容流、SVG、XML、CSV 和库里大多数 JSON 背后的 invariant 数字格式化器——会缩放输入再调 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,其余写 invariant 数字。PLJSONNumber 现在支撑 GetSimilarImageDeduplicationReportJSON、GetAnnotationHitsJSON 以及条码、纠斜(deskew)、结构化文本和 PDF/VCR 报告;纠斜报告以前对非有限角度写 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 为 NaN
// 或 Infinity 写 null,且总是用小数点
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 产品页