PDF/R,标准化为 ISO 23504-1,是为扫描文档而设的 PDF profile:每一页恰好携带一张带状图像(strip image),别无其他。PDFium Component 从 Delphi、Lazarus 和 C++Builder 通过 ValidatePdfRCompliance 校验它,该函数读取一个流,返回合规级别外加一组具体问题
这个 profile 之所以存在,是因为扫描仪和文档采集系统需要一个比 PDF/A 更窄的目标。一份归档 PDF 可以装下该子集允许的任何东西;一份光栅 PDF 则是刻意地贫乏,所以任何合规阅读器都能同样地显示它,任何合规写入器都能在没有创作引擎的情况下从一次扫描产出它
PDF/A 允许而 PDF/R 禁止的是什么
实际上是文本。一张光栅页面携带扫描图像、别无其他,所以页面上出现一个字体资源就是违规——在 ISO 23504-1 §6.5.2 之下被报告为 pvriFontForbidden。这让那些为可搜索性加上一层不可见 OCR 文本层的人感到意外,那在 PDF/A 工作流里是一件正常而有用的事,但根本就不是 PDF/R
页面与图像的关系同样严格。§6.5.1 让每一页恰好是一张带状图像,所以当图像数量与页面数量不匹配时 pvriPageImageMismatch 触发——一张没有图像的页面和一张有两张图像的页面都不合规。而 pvriBadMediaBox 报告一张 MediaBox 不具有 [0 0 w h] 形式的页面(§6.5.3),因为一份扫描没有理由坐在偏移的原点上
uses FPdfPdfr;
var
Src: TFileStream;
Res: TPdfRValidationResult;
begin
Src := TFileStream.Create('scan-batch-0142.pdf', fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfRCompliance(Src);
if Res.IsCompliant then
Memo1.Lines.Add('PDF/R-1 conformant')
else
begin
if pvriFontForbidden in Res.Issues then
Memo1.Lines.Add('A page names a font resource; a raster page carries no text');
if pvriPageImageMismatch in Res.Issues then
Memo1.Lines.Add('Image count does not match page count');
if pvriForbiddenImageFilter in Res.Issues then
Memo1.Lines.Add('A strip image uses an encoding outside the white list');
end;
finally
Src.Free;
end;
end;
允许哪些图像编码
四种,白名单很短是有原因的。§6.6 接受 /CCITTFaxDecode、/DCTDecode、/JPXDecode 和 /FlateDecode——二值传真、JPEG、JPEG 2000 和无损 deflate,它们合起来覆盖了每一种要紧的扫描仪输出。其他一切都作为 pvriForbiddenImageFilter 报告,包括 /LZWDecode、/RunLengthDecode、/ASCII85Decode、/ASCIIHexDecode、/JBIG2Decode 和 /Crypt
其中两条拒绝值得去理解而不是死记。/JBIG2Decode 把二值扫描压缩得极好,在 PDF/A 里完全合法,但它的符号字典重建可能替换成视觉上相似的字形——一种有据可查的扫描数字失败模式——而一个全部立意就在于忠实光栅复现的 profile 不能接受这种风险。ASCII 过滤器被排除则是出于相反的原因:它们让文件膨胀,却不带来任何光栅 profile 需要的东西
在任何一页被读取之前就会触发的结构规则
PDF/R 也约束容器。pvriObjStmPresent 报告一个 /Type /ObjStm 流,该 profile 完全禁止它——对象流让一台光栅阅读器本应能执行的简单、顺序解析变得复杂。pvriBadHeader 报告 %PDF-1.4 到 1.7 以及 %PDF-2.0 之外的头部,而 pvriEncryptVersionMismatch 按 §6.2.3 报告一份头部不是 %PDF-2.0 的加密文件
Catalog 和 Info 字典是白名单式而非仅被检查。pvriProhibitedCatalogEntry 和 pvriProhibitedInfoEntry 对允许集合之外的条目触发,pvriInfoXmpMismatch 在一条 Info 条目与其 XMP 对应物不一致时触发。一个缺失的 Catalog /Metadata 流、一个缺失的 trailer /ID 以及一个缺失的 %PDF-raster-1.0 页脚标记,也各有各的问题
为什么保存选项记录里没有 Title 和 Author
TPdfRSaveOptions 携带 Creator、Producer、CreationDate、ModDate、DocumentId 和 InstanceId,并刻意没有 Title、Author、Subject 或 Keywords 字段。这四项正是 §6.4.3 禁止的条目,所以一条暴露它们的记录,会邀请调用方通过一个合规 API 写出一份不合规文件
两个布尔选项控制在转换既有 PDF 时的清理。StripInfoOptionalEntries 默认为 True,从源 Info 字典移除 Title、Author、Subject、Keywords 和 Trapped。StripCatalogOptionalEntries 同样默认为 True,移除 Names、Outlines、StructTreeRoot、OutputIntents、Lang 等其余项,只留下 §6.3 白名单。把任何一个设为 False,你就保留了那些条目——并失去合规,这偶尔确实是一份内部文件所想要的
var
Opts: TPdfRSaveOptions;
Src, Dest: TFileStream;
begin
Opts := TPdfRSaveOptions.Default;
Opts.Creator := 'Capture Station 4';
Opts.Producer := 'PDFium Component';
Src := TFileStream.Create('scan-in.pdf', fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create('scan-pdfr.pdf', fmCreate);
try
InjectPdfRMarkers(Src, Dest, Opts); // markers + metadata, not page content
finally
Dest.Free;
end;
finally
Src.Free;
end;
end;
请注意标识注入不做的事:它加上元数据和标识,无法提供页面内容。一张不携带带状图像的源页面在注入之后仍会因 pvriPageImageMismatch 失败,因为缺的那张图像从来就不是元数据问题
PDF/R 在采集流水线里的位置
用在交付物就是扫描本身、保真度就是全部契约的场合——取证影像、支票与汇款采集、来自大幅面扫描仪的工程图归档。一旦文档需要可搜索的文本、标签化、嵌入附件或任何其他被光栅 profile 剥去的东西,就改用 PDF/A
一种常见而可行的安排是两者都产出:一份永不更改的 PDF/R 原件,加一份带 OCR 层用于检索的 PDF/A 衍生件。两个校验器相互独立,所以同一个批量任务可以把每件产物对着它实际声称的 profile 检查。关于那对组合中的归档侧,参见 PDF/A 归档合规和 PDF/A 预检校验的笔记;对于面向印刷的输出,参见 校验印刷就绪的 PDF/X 文档的详解
PDFium Component 把 PDFium 引擎带给 Delphi、C++Builder 和 Lazarus,配以 VCL API 以及针对 PDF/A、PDF/X、PDF/E、PDF/UA 和 PDF/R 的合规校验器——PDFium Component 产品页列出了支持的标准与 IDE 版本