某個理賠處理團隊把三十年的紙本檔案送進一台饋紙式掃描器。掃描器把每一頁吐成一個 JPEG 丟進資料夾,命名為 0001.jpg、0002.jpg,依此類推。而封存庫真正需要的,是每個案件檔一份 PDF、頁序正確,好讓審閱者打開單一份文件,而不是在上百張影像縮圖之間點來點去。最後那個步驟,也就是把一堆編號的掃描檔變成一份有序的 PDF,正是這裡要做的事
PDFium Component 直接處理得來。除了呈現與文字擷取之外,這個元件還能從零建置一份 PDF:建立空白文件、加入一頁您想要的尺寸的空白頁、以使用者空間座標把影像放上那一頁,然後存檔。整條流水線都活在 TPdf 元件上,所以一支批次轉換程式,就是一個檔名迴圈加上少少幾次呼叫
這趟轉換的形狀
每張掃描檔都得發生三件事。您決定頁面尺寸,把影像放進頁面內並留下邊界,然後前進到下一頁。PDFium Component 各給您一個方法:AddPage 以指定尺寸建立一頁空白頁,AddImage(若您手上已有 TPicture,則用 AddPicture)把點陣圖畫進目前頁面,而 PageNumber 告訴元件後續的繪製呼叫要瞄準哪一頁
唯一會絆倒人的細節是座標系統。PDF 使用者空間把原點放在頁面左下角,Y 值向上遞增,與 Delphi 開發者反射性伸手去拿的螢幕座標相反。您傳給 AddImage 的 X, Y 是影像矩形的左下角,而 Width, Height 是以點為單位的放置尺寸,不是來源檔案的像素尺寸。搞反了,您的掃描檔就會落到頁面外,或是相對於您預期的位置上下顛倒
建立文件,並為每張掃描檔各建一頁
從一份空白文件開始。CreateDocument 會配置一份全新的 PDF,並讓元件保持在作用中,所以不需要另外一個開啟步驟。接下來您走過掃描檔清單,為每一份加一頁、把它設為目前頁,再放上影像。這裡的頁面尺寸是以點為單位的 A4(直式 595 × 842),也就是封存函件的標準紙張尺寸
procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
PageW = 595.0; // A4 寬度,單位為點
PageH = 842.0; // A4 高度,單位為點
Margin = 36.0; // 每張掃描檔四周半英吋的邊界
var
I: Integer;
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // 全新、空白、已在作用中
for I := 0 to Files.Count - 1 do
begin
Pdf.AddPage(I + 1, PageW, PageH); // 頁索引以 1 為起點
Pdf.PageNumber := I + 1; // 把新頁設為目前頁
PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
end;
Pdf.SaveAs(OutputPath);
finally
Pdf.Free;
end;
end;
每一輪迭代都建立一頁,並立刻把 PageNumber 設到它身上。第二行很要緊:AddPage 插入了頁面,但繪製方法作用的是目前那一頁,所以設定 PageNumber 才是讓 AddImage 瞄準您剛做好那一頁的動作。省掉它,您的影像就會全疊到先前碰巧載入的那一頁上
那個迴圈裡藏著一項假設:Files 的順序。掃描器把頁面命名為 0001.jpg 到 0100.jpg,但目錄列舉不見得回傳排好序的結果,而只要碰上 page9.jpg 跟 page10.jpg 並排,單純的字串排序就會把第 10 頁排到第 9 頁前面。請在迴圈之前明確把清單排序,並在掃描時就優先採用補零的檔名,讓字典序與頁序一致。頁面順序是審閱者一眼就會注意到的東西,也是最便宜就能預防的錯誤
放上掃描檔並維持它的長寬比
掃描檔很少和頁面同一個形狀。若您把它拉伸到填滿整張紙,文字就變形了;若您以完整像素尺寸放上去,它又會溢出。解法是取寬度貼合與高度貼合兩個比例中較小的那個來縮放,再把剩下的空間置中。由於原點坐在左下角,置中意味著把剩餘空間平均對分,並同時加到 X 與 Y 上
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
PageW, PageH, Margin: Double);
var
Pic: TPicture;
AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
Pic := TPicture.Create;
try
Pic.LoadFromFile(FileName); // 透過 VCL 圖形單元支援 BMP、JPG、PNG 等格式
AvailW := PageW - 2 * Margin;
AvailH := PageH - 2 * Margin;
// 在邊界內貼合,且不讓掃描檔變形。
Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
DrawW := Pic.Width * Scale;
DrawH := Pic.Height * Scale;
// 置中:剩餘空間平均對分。Y 從頁面底部量起。
X := (PageW - DrawW) / 2;
Y := (PageH - DrawH) / 2;
Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
finally
Pic.Free;
end;
end;
這段程式把檔案載入一次以讀出它的像素尺寸,算出單一的等比縮放值,再把放置矩形傳給 AddImage。AddImage 直接接受檔案路徑,並把它導進與 AddPicture 相同的影像流水線,所以 VCL 圖形單元認得的任何格式都能用,不必特例處理。若您已經在預覽窗格中把影像解碼進一個 TPicture,就用同一個矩形呼叫 AddPicture(Pic, X, Y, DrawW, DrawH),省掉第二次讀檔
為 JPEG 掃描檔省掉解碼
掃描器幾乎都輸出 JPEG。把 JPEG 載入 TPicture 會把它解碼成點陣圖,之後 PDFium 在存檔時又重新編碼一次,這是兩趟您不需要的失真往返。AddJpegImage 直接從串流把原始的壓縮位元組內嵌進頁面,對高量批次而言,這既更快,視覺上也更乾淨
var
Stream: TFileStream;
begin
// ... 在目前頁面完成 AddPage 與 PageNumber 之後 ...
Stream := TFileStream.Create(FileName, fmOpenRead);
try
// 原樣內嵌 JPEG 位元組;沒有解碼與重新編碼的循環。
Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
finally
Stream.Free;
end;
end;
您仍然要用同樣的方式算出 X、Y、DrawW 與 DrawH,因為縮放需要像素尺寸。請從檔案讀出它們,或快速剖析一下檔頭,再把原始串流交給 AddJpegImage。若掃描檔是 PNG 或 TIFF,AddImage 那條路才是對的;JPEG 這條捷徑就留給它真正適用的格式
為每一頁加上標示
當每一頁都帶著它的來源檔名時,封存的掃描檔就更容易稽核。AddText 會在使用者空間座標上畫出一段字串,所以說明文字就坐在影像正下方。別忘了倒過來的 Y 軸:要把標示放在掃描檔下方,您是從影像的下緣減去,而不是加上去
// 掃描檔下方的說明文字:Y 值愈往頁面底部愈小。
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
X, Y - 14, clGray);
關於存檔的最後一點。SaveAs 是一個回傳 Boolean 的函式,所以在生產程式碼裡請檢查它的結果,而不是假設寫入成功了;否則磁碟滿了或輸出路徑被鎖住時,它會無聲地失敗。迴圈跑完、檔案寫出之後,您拿到的正是封存庫需要的東西:每個案件檔一份有序的 PDF、頁面都縮放到剛好貼合,在任何檢視器裡都能讀
同一組積木也能應付相關的工作。換掉逐頁的尺寸規則,您就得到一本每張紙一張照片的相片書;保留迴圈但改從多頁 TIFF 來源讀取,您就有了一支傳真封存轉換器。若您想看看以程式建置 PDF 的全貌,請參閱用 PDFium Component 從零建立 PDF 文件;若之後要把結果重新呈現到螢幕上,請參閱用 PDFium Component 把 PDF 頁面轉成 JPEG 影像
來自 loslab.com 的 PDFium Component 打包了本系列通篇使用的文件建立、呈現與文字 API