losLab PDF Library for Delphi 用 TPDFlib.CompressPageAsMRC 以 Mixed Raster Content 方式压缩扫描页:方法先渲染页面,对全分辨率的 1 位前景蒙版做阈值化,把其余部分降采样成 JPEG 背景,再用两张图像重建页面——一层 /DCTDecode 背景,加一层用当前填充色涂色的 /CCITTFaxDecode 模板蒙版
这个方法为之存在的页面,每个文档档案库里都堆成山:300 DPI 的发票或合同彩色扫描件,存成一张大 JPEG。质量调到文字还看得清,文件就很大;质量一降,字母周围长出振铃光晕、衬线被抹糊,因为 JPEG 最不擅长的事就是白纸上的黑色硬边。Mixed Raster Content 不再让一个编解码器干两份活
为什么单个 JPEG 会让扫描文字又糊又大?
单个 JPEG 处理扫描文字很糟糕,因为 DCT 编解码器把比特花在平滑渐变上,硬边则用高频系数近似,而这些系数在量化时最先被扔掉。扫描件上的文字几乎全是硬边。要让这些边干净,就得整页用高质量设置——连纸纹、logo、咖啡渍一起高质量,而它们根本不需要
MRC 的思路是按每个区域实际是什么来拆分页面。墨迹变成二值图像,CCITT 这类传真编解码器在其中无损且非常紧凑地记录黑白像素游程。其余一切——纸色、照片、彩色背景——变成连续色调图像,可以用零头一样的分辨率存储,因为没人会凑到 300 DPI 去看背景。二值层让文字在全分辨率下保持锐利;背景层把颜色以低成本留住
CompressPageAsMRC 怎么把一页拆成两层?
TPDFlib.CompressPageAsMRC 对每页跑一条固定管线。它按请求的 DPI 把页面渲染成 24 位位图,用 Rec. 601 加权亮度(千分比 299/587/114)对照 Threshold 给每个像素分类,并把结果写进同分辨率的 1 位蒙版。同时它用 BGScale 做盒式均值把同一份渲染缩小,BGScale 为 4 时每个 4×4 像素块变成一个背景像素
随后页面内容被替换。原操作符被移除,降采样背景以 /DeviceRGB 的 /DCTDecode 图像嵌入(ISO 32000-1 §7.4.8),蒙版用库自带的 CCITT 编码器编码成 /CCITTFaxDecode 图像(§7.4.6)并标记 /ImageMask true,两者铺满整个页面框,蒙版在上。签名很短:
function CompressPageAsMRC(Page: Integer; DPI: Double;
BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
Page从 1 起计,必须落在PageCount之内DPI是源光栅的渲染分辨率,50 到 1200BGScale是背景降采样因子,1 到 8JPEGQuality是背景 JPEG 质量,1 到 100Threshold是亮度切点,1 到 255;不低于它的像素算纸,更暗的算墨Options保留,必须为 0
对一个扫描文件跑典型批处理是下面这样。每页之前先调一次 SetFillColor,原因下一节解释
uses
SysUtils, PDFlibrary;
procedure CompressScannedFile(const SrcFile, DstFile: string);
var
Lib: TPDFlib;
P, Replaced: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SrcFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0); // 模板层的墨色
// 300 DPI 蒙版、75 DPI 背景、JPEG 质量 60、亮度 160 处切分
Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
if Replaced = 0 then
raise Exception.CreateFmt('MRC failed on page %d', [P]);
end;
if Lib.SaveToFile(DstFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [DstFile]);
finally
Lib.Free;
end;
end;
为什么前景要用当前填充色来涂?
前景层是模板蒙版,而模板蒙版自己没有颜色。ISO 32000-1 §8.9.6.2 把 /ImageMask true 的图像定义为 1 位形状:样本值标出颜料该落在哪里,颜料则是图形状态里当前的非描边色。正是这一点让这一层便宜,也是 CompressPageAsMRC 不收颜色参数的原因
TPDFlib.CompressPageAsMRC 用 SetFillColor 设置的颜色涂蒙版,所以调用顺序要紧。涂色是惰性发出的:SetFillColor 记下颜色并标记已变更,模板绘制只在该变更仍处于待发状态时,才把 rg 操作符写进自己的 q/Q 块。中间若夹了一次填充图形的绘制,待发的变更就被那个图形消耗掉了。像上面的批处理那样,每页之前直接设色,就没有猜的余地。黑色是常规选择;蓝黑墨水表单想要输出看起来不那么像复印件,深海军蓝也行
// 蓝色圆珠笔表单:墨层用深海军蓝渲染
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);
摆放跟随库的坐标原点。CompressPageAsMRC 读取 GetOrigin,从页面框顶边起画两张图:原点在左上(SetOrigin(1))时顶坐标为 0,默认左下原点时顶坐标为 PageHeight。你不需要调整任何东西,但它解释了无论你的管线原本用哪个原点,两层都能落对位置
模板语义的一个后果值得直说:被归类为墨的每个像素都取同一个颜色。红色印章、蓝色签名、黑色正文,全都在前景层落进同一个填充色。只有背景 JPEG 在底下还留着原始颜色的淡淡的低分辨率痕迹
DPI、BGScale、Threshold 与 JPEG 质量怎么选
DPI 决定文字的锐度,办公扫描件取 300 是合理的默认值。蒙版保持全渲染分辨率,所以 DPI 决定多细的笔画能挺过阈值化。超过原始扫描分辨率再往上调没有收益,因为渲染器只会对嵌入的扫描图像做上采样。内存是另一个限制:US Letter 页面在 300 DPI 下渲染成 2550×3300 像素,即 25,245,000 字节的 24 位像素数据,蒙版和背景还没算;同一页面在 1200 DPI 上限是 10200×13200 像素,约 404 MB
BGScale 用背景细节换体积。300 DPI 下因子 4 给出 75 DPI 背景,Letter 尺寸即 638×825 像素,对纸色和平坦色块绰绰有余。读者真的会去看的照片页或半调插图值得取 2;白纸上的纯文字可以取 6 或 8,因为背景几乎只剩一层色调
Threshold 是改变页面观感的那个参数。调高它会把更多中间灰像素归为墨,细笔画和铅笔痕迹变粗,但扫描噪声和纸张阴影也跟着变成斑点。调低它纸面更干净、字母更细,有时断笔。黑字白纸取 128 到 170 附近是合理的起步区间;这里的取舍与 在 Delphi 里把 PDF 页面渲染成 1 位单色一文讨论的是同一件事,同一个亮度阈值驱动着 RenderPageToMonochromeFileEx。与那个方法不同,CompressPageAsMRC 没有去斑步骤,噪声大的扫描件适合略低一点的阈值
JPEGQuality 不像单图扫描时那么要紧,因为背景不再承担任何文字边缘。75 DPI 背景藏在锐利蒙版后面,质量 50 到 70 通常看不出来
返回值告诉你什么,又不告诉你什么?
TPDFlib.CompressPageAsMRC 返回它替换掉的页面内容的字节数,失败时返回 0。它是成功指示器,不是节省量的度量。典型扫描页的原始内容流只有几十个字节——放一张图所需的操作符而已——所以即使页面缩小了几兆,返回一个小的正数也是正常结果。原始内容流为空的页面即使被重建了也报 0
失败行为不是原子的,这是要在设计里绕开的部分。参数检查、渲染和 JPEG 编码都在碰页面之前运行,所以越界的 BGScale、101 的 JPEGQuality 或渲染失败都让页面保持原样。但一旦原内容被清掉,嵌入某一层或绘制失败同样返回 0,此时页面可能已被部分重建。任何 0 都当作丢弃内存中文档的理由而不是保存它,并且永远不要把结果写回源文件
想知道这次转换到底省了多少,去量文档。比较 SaveToString 前后的长度是最快的检查;想按类别看,用 AuditDocumentSpace 做 PDF 文件体积审计能显示重量是否如期从图像里挪了出去
function MRCSavings(Lib: TPDFlib): Int64;
var
Before: Int64;
P: Integer;
begin
Before := Length(Lib.SaveToString);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0);
if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
end;
Result := Before - Length(Lib.SaveToString);
end;
这套双层 MRC 距 ITU-T T.44 还差在哪
CompressPageAsMRC 实现的是刻意简化的 MRC。ITU-T T.44 的完整模型每个条带用三层:背景图像、承载墨色的前景图像、逐像素在两者之间选择的二值蒙版,一页上还允许多组蒙版加前景。放到 PDF 术语里,第三层应当是带显式蒙版(§8.9.6.3)的低分辨率彩色前景图像,而不是单色涂的模板。PDF Library for Delphi 略去了前景色层——结果因此如此之小,彩色墨并进填充色也是因为这个
- 这个方法适合扫描纸面上的黑色或单色文字;按颜色编码的文档、荧光标注和彩色印章会丢掉墨色
- 重建后的页面是纯光栅:原页面上的真实文字、矢量图和可选中的字符都变成像素,所以拿它处理扫描件,别用在原生数字页面上——那里 字体子集化与图像降采样才是对的工具
- 蒙版用库的 CCITT 编码器、T.4 二维模式,写成带
/K 1的/CCITTFaxDecode;编码器经AddImageCCITTFromBitmapStream还有 Group 4 模式,但CompressPageAsMRC没有暴露它,也不使用 JBIG2 - 注解和表单字段不被压平;它们作为对象留在重建内容之上
- 背景是
/DeviceRGB,所以输出不适合 PDF/X-1a 工作流,而且 PDF/X-1a 模式下SetFillColor本身就会被拒绝 - 本就需要 OCR 的扫描件应当先识别再 MRC,趁原始像素还在全色深时
FAQ
CompressPageAsMRC 保留文字可搜索吗?
不保留。CompressPageAsMRC 用两张图像替换页面内容,原页面上的文字操作符连同隐形的 OCR 文本层一起被移除。压缩之后对页面重跑 OCR,或压缩后把文本层加回去
可以只压缩文档里的部分页面吗?
可以。CompressPageAsMRC 每次处理一个从 1 起计的页面,页面由你挑,甚至可以逐页调整 BGScale 和 Threshold,比如照片插页取 2、纯文字页取 6
Options 参数该传什么?
传 0。当前版本里 Options 保留作未来用途,没有定义任何位,非零值在今天不携带任何含义
CompressPageAsMRC 随 losLab PDF Library for Delphi v3.561.0 发布,构成它的渲染、JPEG、CCITT 和模板蒙版构件也一并到位——需要管线的变体时,它们都是公开方法。库本身、C++Builder 支持和完整 API 参考见 PDF Library for Delphi 产品页