如果需要以 600 DPI 將 PDF 轉換為 JPG 以進行列印、存檔或高解析度擷取,簡短的答案是:可行,但必須限制輸出尺寸。大型 PDF 頁面以 600 DPI 轉換可能會產生每頁數 GB 的 JPG,導致任何未設定記憶體上限的伺服器當機。我在 Convertify 的生產環境中遇到這個問題,因此實作了一個解決方案,既能盡量滿足使用者需求,又不會讓程序崩潰。

本文將介紹我在 Rust + libvips 中撰寫的限制函式、背後的數學原理(關鍵字:sqrt)、實際生產數據,以及取捨考量。如果你只想使用這個工具,這裡是 600 DPI PDF 轉 JPG 轉換器。免費、無需註冊,文中所述的限制機制會套用於每一筆請求。

為什麼 600 DPI 很重要

在「PDF 轉圖像」的脈絡中,DPI 並非 PDF 的物理屬性,而是一個縮放因子。當你指定 libvips 以 600 DPI 載入 PDF 頁面時,代表要依照原始文件尺寸,以每英吋 600 像素進行光柵化。因此,輸出尺寸完全取決於 PDF 本身認為自己有多大。

大多數情況下,300 DPI 就足夠了。列印、大多數雜誌、高品質照片輸出,300 DPI 已是標準。600 DPI 通常出現在以下情境:

  • 存檔掃描。 你希望保留原稿的每一處細節。
  • 大型格式的高品質列印: 海報、技術圖面、工程藍圖。
  • 密集排版的 OCR。 小字體與特殊字形需要更高的光柵解析度。
  • 大型文件。 地圖、藍圖、寬幅設計圖。這也是我首次遇到問題的地方。這些 PDF 常有巨大的原生尺寸(24 吋 × 36 吋或更大),以 600 DPI 轉換就會爆炸。

如果有人在搜尋引擎輸入 pdf to jpg 600 dpi,通常就有上述需求。他們不希望工具在未告知的情況下悄悄降級至 300 DPI。

樸素做法及其失敗原因

在 libvips 中以指定 DPI 載入 PDF 頁面只需一行:

let img = VipsImage::new_from_file(&format!("{file_path}[dpi=600,page={page}]"))?;

Enter fullscreen mode Exit fullscreen mode

就是這樣。libvips-poppler 負責渲染。在一般 A4 頁面上,會得到約 3500 萬像素的影像,JPG 大小約 8–15 MB,沒問題。

現在換成大型 PDF,例如 24 吋 × 36 吋的技術圖面。同樣的程式碼會產生超過 3 億像素的影像。原始像素資料在記憶體中就超過 1 GB,以品質 90 的 JPG 編碼仍可能達到每頁 200–400 MB。多頁 PDF 以 600 DPI 轉換:你會面對數 GB 的輸出,通常在使用者還來不及反應前就已發生。

我在生產環境中看到的實際症狀:

  • 伺服器因記憶體不足而中斷轉換。
  • 下載開始卻因檔案太大而無法完成。
  • 使用者抱怨「小」PDF 卻產生超大輸出。

最偷懶的做法是全域限制 DPI 為 300。但這會懲罰正常使用情境:需要 600 DPI 的普通尺寸 PDF 卻因為少數特例而被迫降級。因此,我需要一個能自適應的方案。

百萬像素預算

真正的限制並非 DPI,而是輸出像素數。3500 萬像素的 JPG 和 3.5 億像素的 JPG 是完全不同的東西,無論是用什麼 DPI 產生。

因此,我沒有直接限制 DPI,而是設定「百萬像素預算」:輸出影像不得超過 N 百萬像素。在每次轉換前,先以低 DPI 探測頁面尺寸,計算請求的 DPI 產生多少像素,若超過預算,則適度降低 DPI 以符合限制。

在 Convertify 上,我的預算目前是 100 MP。這對一般文件的 600 DPI 來說綽綽有餘(標準信紙尺寸以 600 DPI 僅約 34 MP)。只有在病態案例——超大 PDF 或極端尺寸——才會啟動。

限制函式

以下是實際上線的函式:

pub fn clamp_dpi_to_megapixels(
    file_path: &str,
    page: i32,
    requested_dpi: i32,
    max_megapixels: f64,
) -> i32 {
    let probe = VipsImage::new_from_file(&format!("{file_path}[dpi=72,page={page}]"));

    let (w, h) = match probe {
        Ok(img) => (img.get_width() as f64, img.get_height() as f64),
        Err(_) => return requested_dpi,
    };

    let scale = requested_dpi as f64 / 72.0;
    let mp = (w * scale) * (h * scale) / 1_000_000.0;

    if mp <= max_megapixels {
        requested_dpi
    } else {
        let factor = (max_megapixels / mp).sqrt();
        ((requested_dpi as f64 * factor).floor() as i32).max(72)
    }
}

Enter fullscreen mode Exit fullscreen mode

讓我逐步說明。

以 72 DPI 探測。 libvips-poppler 將 72 DPI 視為 PDF 的原生「點」網格,以 72 DPI 載入成本低,且能取得頁面的真實尺寸(單位為點)。8.5 吋 × 11 吋的頁面會回傳 612 × 792 像素。

若探測失敗(頁碼超出範圍、PDF 損毀等),直接回傳請求的 DPI。後續轉換會產生適當的錯誤訊息,這不是吞掉錯誤的地方。

計算請求 DPI 會產生的像素數。 將基礎尺寸乘以 requested_dpi / 72,再換算為百萬像素。同樣的 8.5 吋 × 11 吋頁面以 600 DPI 計算: (612 × 8.33) × (792 × 8.33) / 1_000_000 ≈ 33.6 MP。遠低於預算。

未超過預算則原樣回傳。 若 naive DPI 產生的 MP ≤ 最大值,就結束。這是快樂路徑,估計占 95% 以上的使用者請求。

超過預算則按比例降低。 這裡的數學很重要。要將 350 MP 降到 100 MP,需要縮小 100/350 倍。但這個比例是針對像素。DPI 與像素的關係是平方:DPI 減半,像素變四分之一。因此 DPI 的縮放因子是 sqrt(100/350) ≈ 0.53。套用至 600 DPI: 600 × 0.53 ≈ 318 DPI。仍然高於安全下限 300,但已低到可接受。

.floor().max(72) 是安全機制。floor 避免四捨五入超過預算;72 是有效下限,因為低於 PDF 原生網格的光柵化會產生垃圾。

為什麼需要 sqrt(簡短數學說明)

這裡沒有什麼高深,但值得清楚說明,因為這是整個洞見的核心。

像素數量隨 DPI 的平方成長,而非線性。DPI 加倍,寬高都加倍,像素變四倍。DPI 變三倍,像素成長 9 倍。關係式如下:

pixels = base_pixels × (dpi / 72)²

Enter fullscreen mode Exit fullscreen mode

已知目標像素數,求 DPI:

dpi = 72 × sqrt(target_pixels / base_pixels)

Enter fullscreen mode Exit fullscreen mode

這正是限制函式所做的,只是透過 requested_dpi 進行調整。如果你跳過 sqrt,直接用像素比例乘以 DPI,會過度修正,最後得到遠低於預算允許的 DPI,使用者得到的解析度比實際需要低很多。

我曾在網路上看到「按比例縮放 DPI」的限制寫法,結果輸出解析度比應有的低很多。原因就在這裡。

實際生產數據

以下是 100 MP 預算下,限制函式的實際行為:

輸入文件 72 DPI 基礎尺寸 請求 DPI 請求 DPI 下的 MP 限制後 DPI 輸出 MP
Letter (8.5 吋 × 11 吋) 612 × 792 (0.48 MP) 600 33.6 MP 600 33.6
A3 (11.7 吋 × 16.5 吋) 842 × 1191 (1.0 MP) 600 69.6 MP 600 69.6
大型藍圖 (24 吋 × 36 吋) 1728 × 2592 (4.5 MP) 600 311 MP 340 100
寬幅設計圖 (36 吋 × 60 吋) 2592 × 4320 (11.2 MP) 600 778 MP 215 100

模式很清楚:在一般文件上,600 DPI 會原封不動通過。在 naive 做法會產生數億像素的邊緣案例,DPI 會被縮放到符合預算的使用者仍能得到比全域 300 DPI 更接近原本意圖的輸出。

什麼時候應該改用 TIFF

值得一提:如果你要以 600 DPI 進行存檔,JPG 通常不是最佳選擇。JPG 是有損壓縮,而高 DPI 時你通常是為了保留細節才需要無損:文字邊緣、細線、小註記。

這種情況 TIFF 才是正確格式。LZW 壓縮的 TIFF 在合理檔案大小下仍能無損,且全球所有存檔系統都支援。libvips 原生支援 TIFF。

我在 PDF 轉 TIFF 轉換器 中也使用了相同的限制機制,只是換了編碼器。如果你正在打造類似工具,請不要強迫 600 DPI 的存檔工作使用 JPG,也請支援 TIFF。你的使用者會感謝你。

如果重來一次,我會改變什麼

兩件事。

每頁回饋。 目前限制機制是靜默運作。一份 200 頁的文件,其中一張地圖從 600 DPI 被降到 340 DPI:使用者看不到。對大多數情況來說,靜默是友善的,但對於存檔工作流程,人們希望知道哪些頁面觸發了限制。我會在回應中顯示這些資訊。

自適應預算。 100 MP 是硬編碼常數。在更大的伺服器上可以是 250 MP;在純行動裝置的 session 中,40 MP 可能更合理。從請求情境推導預算(或讓使用者選擇更高階方案)只需小小的設定變更,就能帶來實質影響。

這兩件事都不急迫,但都在待辦清單上。

立即試用

如果你現在就需要 600 DPI 的 PDF 轉 JPG:Convertify 可以做到。免費、無需註冊、支援最多 10 個檔案批次處理,上述限制函式會套用於每一筆請求。上傳超大檔案,你會得到符合記憶體預算的最高 DPI。不會當機,也不會默默全域降級。

如果你正在打造自己的 PDF 轉圖像工具,也遇到了類似問題,上述函式已在生產環境驗證過,程式碼短小精悍,可直接複製使用。調整 max_megapixels 以符合你的基礎設施,然後繼續前進。

如果你想看更多 Convertify 的工程細節,我每週都會在 Building Convertify in Public 這個帳號發文。libvips 技巧、Rust 模式、真實生產錯誤,沒有廢話。