如果需要以 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 模式、真實生產錯誤,沒有廢話。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.