PDF 的頁面尺寸在頁面建立當下就固定了,所以您沒辦法像調整影像大小那樣,直接就地重新縮放內容。讓縮小這件事變得可行的程式庫模型是擷取後重畫:把每一頁的內容從文件中提取到一個控制代碼裡,以原本的媒體尺寸建立一頁全新的空白頁,再把擷取到的內容以縮小後的定界方框畫回去。四周的留白就成了邊界。舉例來說,在 A4 頁面上以 70% 縮放時,左右各吃掉寬度的 15%,上下也是同樣的比例,而這正是下面的邊界算式算出來的結果
CapturePage 怎麼運作
CapturePage 接受一個頁碼,把該頁的內容提升成一個記憶體中的擷取物件,並把該頁從文件的頁面樹中移除。這個移除是刻意為之,也正是為什麼不管迭代索引是多少,迴圈永遠選第 1 頁:第 1 頁一旦被擷取並刪除,原本的第 2 頁就成了新的第 1 頁,依此類推。如果您把頁面選擇器跟著迴圈計數器一起遞增,就會每隔一頁跳過一頁,最後只拿到預期輸出的一半
CapturePage 回傳的擷取控制代碼不是頁面參照,它比較像一份內容快照。它會一直有效,直到您呼叫 DrawCapturedPage 或明確釋放它為止。DrawCapturedPage 接受那個控制代碼,外加一個以左偏移、下偏移、寬度與高度表示的目的地矩形,單位都是點。程式庫會把擷取到的內容縮放到剛好填滿那個矩形,唯有當您的矩形恰好符合原本的比例時,長寬比才會被保住。若要等比例縮放,您要的矩形就是原尺寸乘上縮放係數,並且置中於頁面上
置中的算式
在 70% 的縮放係數下,每個維度剩下的 30% 會平均分給兩側。所以水平內縮量是 pageWidth * (1.0 - 0.70) / 2,也就是寬度的 15%,垂直內縮量則以頁高套用同一條算式。DrawCapturedPage 的目的地矩形因此起始於 (horizBorder, vertBorder),並橫跨 pageWidth - 2 * horizBorder 乘以 pageHeight - 2 * vertBorder。這套算術與程式庫無關,它就只是把一個較小的矩形對稱地塞進較大矩形裡的幾何
有一點值得注意:SetOrigin(1) 會把座標原點放在左上角,而不是左下角。您傳給 DrawCapturedPage 的邊界值,是從您設定的那個原點量起的,所以如果您在載入與繪製之間切換了原點模式,置中就會跑掉
C# 範例
下面這段程式碼把 Pages.pdf 的每一頁都跑過擷取後重畫的循環,並把結果寫進 newpages.pdf。PDFL 是從 PDFlibDLL64.dll 加進專案的 ActiveX/COM 包裝物件
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// 永遠選第 1 頁:CapturePage 會移除該頁,所以下一輪
// 迭代時第 2 頁就變成第 1 頁。
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Delphi 範例
Delphi 版本直接使用 TPDFlib,而不透過 COM 層,但呼叫順序完全相同。一個實務上的差異是輸出檔案的防護:用 FileExists 加上 DeleteFile,而不是 File.Delete,因為若目的地檔案被上一輪執行、且仍開在檢視器中的程序鎖住,SaveToFile 就會失敗
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
縮放係數實際控制的是什麼
這裡的 0.70 意思是呈現出來的內容佔每個頁面維度的 70%,不是說檔案變成原本位元組大小的 70%。這項操作之後的檔案大小取決於原內容的複雜度;帶著大圖的頁面不會等比例縮小,因為像素資料是以相同解析度重畫進較小的區域裡。若目標是位元組層級的壓縮,正確的作法是 LinearizeFile 或以串流壓縮重新存檔,而不是幾何縮放
70% 這個數字也不是硬性上限。0.0 到 1.0 之間的任何值都可以,而大於 1.0 的值會把內容放大到超出原本的頁面界線,除非您同時加大頁面尺寸,否則會在媒體方框邊緣被裁掉。尺寸混雜的文件天生就處理得來,因為 PageWidth 與 PageHeight 是在算邊界之前逐頁查詢的,所以一份奇數頁是 A4、偶數頁是 A3 的文件,不必任何特例處理,在每種頁面尺寸上都會產生正確置中的輸出
哪些地方會出錯
實務上會碰到兩種失效模式。第一種是輸出檔案還被上一輪執行留在 PDF 檢視器中開著:視平台而定,SaveToFile 會失敗或寫出零位元組,新的輸出永遠落不了地。函式開頭那道刪檔防護在開發時夠用,但在生產流水線裡,先寫到暫存路徑、成功後再改名會更安全
第二種是頁數對不上。由於 CapturePage 會邊處理邊把頁面從文件中移除,迴圈之前從 PageCount() 讀到的數字,才是正確的迭代上限。在迴圈內部呼叫 PageCount() 每一輪都會回傳愈來愈小的數字,導致提早結束,把最後幾頁留著沒處理。範例中的迴圈變數只當作剩餘迭代次數的計數器,它從不用來選頁,因為基於前面解釋過的理由,要選的頁永遠是第 1 頁
本文示範的頁面操作呼叫,包含 CapturePage、DrawCapturedPage 與 SetPageDimensions,都是適用於 Delphi、C#、VB.NET 與 C++ 的 losLab PDF Library 的一部分