技術文章

使用 losLab PDF 函式庫將 PDF 頁面縮放至 70%

PDF 頁面尺寸在建立頁面時是固定的,因此您無法像調整影像大小那樣簡單地就地重新縮放內容;使縮小變得可行的函式庫模型是擷取與重新繪製:將每個頁面的內容從文件中提取到控制代碼中,以原始媒體大小建立一個新的空白頁面,然後將擷取的內容以縮小的邊界框重新繪製進去;周圍的空白部分就變成了邊界;例如,在 A4 頁面上進行 70% 縮放時,寬度的 15% 落在兩側,上下也是相同的比例,這正是下方邊界計算所產生的結果

How CapturePage works

CapturePage 接受一個頁碼,將該頁面的內容提升為記憶體中的擷取物件,並將該頁面從文件頁面樹中移除;該移除是刻意的,這也是無論反覆運算索引為何,迴圈總是選擇第 1 頁的原因:一旦擷取並刪除了第 1 頁,原本的第 2 頁就會變成新的第 1 頁,依此類推;如果您隨迴圈計數器一併遞增頁面選擇器,您將跳過每隔一頁,並最終得到預期輸出的一半

CapturePage 如何運作

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++)
    {
        // Always select page 1: CapturePage removes the page, so page 2
        // becomes page 1 on the next iteration.
        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 的一部分