losLab PDF Library for Delphi 透過 TPDFlib.CompressPageAsMRC 以 Mixed Raster Content 壓縮掃描頁。這個方法先渲染頁面,以門檻值產生全解析度的 1 位元前景遮罩,把其餘部分降取樣成 JPEG 背景,再以兩張影像重建頁面:一個 /DCTDecode 背景與一個以目前填色著色的 /CCITTFaxDecode 模板遮罩
這個功能為之而生的是每個文件典藏庫都堆滿的那種頁面:發票或合約的 300 DPI 彩色掃描,存成一整張大 JPEG。品質設定到文字讀得清,檔案就大;調低品質,字母便長出光暈與糊開的襯線,因為 JPEG 最不擅長的就是白紙上的銳利黑邊。Mixed Raster Content 不再要求一個編碼器同時做兩份工作
為什麼單一 JPEG 會讓掃描文字又糊又大?
單一 JPEG 處理不好掃描文字,是因為 DCT 編碼器把位元花在平滑漸層上,而硬邊則用量化最先丟棄的高頻係數去逼近。掃描件裡的文字幾乎全是硬邊。要把這些邊保乾淨,整頁都得用高品質設定,連紙張紋理、商標與咖啡漬也一併受益,而後面這些根本不需要
MRC 的思路是按每個區域實際上是什麼來切分頁面。墨水變成只有黑與白兩種值的影像,CCITT 這類傳真編碼器以無損且極緊湊的方式記錄黑白行程。其餘一切,紙張色調、照片、彩色背景,則變成連續調影像,可以用解析度的幾分之一儲存,因為沒有人會用 300 DPI 讀背景。雙階層讓文字以全解析度保持銳利;背景層以低代價保住色彩
CompressPageAsMRC 怎麼把一頁拆成兩層?
TPDFlib.CompressPageAsMRC 對每頁執行一條固定的管線。它以要求的 DPI 把頁面渲染成 24 位元點陣圖,用 Rec. 601 加權亮度(每千分之 299/587/114)對 Threshold 分類每個像素,並把結果寫進同解析度的 1 位元遮罩。同時它以 BGScale 按方塊平均縮小同一份渲染,所以 BGScale 為 4 時,每個 4×4 像素塊縮成一個背景像素
頁面內容隨後被替換。原始運算子被移除,降取樣的背景以 /DeviceRGB(ISO 32000-1 §7.4.8)的 /DCTDecode 影像嵌入,遮罩由程式庫的 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 記下顏色並標記為已變更,只有當這次變更還掛著時,模板繪製才會在自己的 q/Q 區塊內寫入 rg 運算子。中間若畫了一個填色圖形,這筆待套用的變更就被那個圖形消費掉了。像上面批次那樣,在每頁之前直接設定顏色,就沒有猜的成分。黑色是常規選擇;藍墨表單想讓輸出看起來不像影印本,可以用深藏青
// 藍色原子筆表單:以深藏青渲染墨水層
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。它是成功指標,不是節省量的度量。典型掃描頁的原始內容流只有幾十個位元組,剛好夠放下擺一張影像的運算子,所以就算整頁縮了好幾 MB,小的正數也是正常結果。原始內容流本來就空的頁面即使被重建也回報 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 二維模式,寫成
/CCITTFaxDecode加/K 1;編碼器透過AddImageCCITTFromBitmapStream也有 Group 4 模式,但CompressPageAsMRC沒有暴露它,也不用 JBIG2 - 註解與表單欄位不會被扁平化;它們以物件形式留在重建後的內容之上
- 背景是
/DeviceRGB,所以輸出不適合 PDF/X-1a 流程,而SetFillColor本身在 PDF/X-1a 模式下也會被拒絕 - 本來就需要 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 產品頁