| 方案 | 優點 | 缺點 | 授權/成本 |
|---|---|---|---|
| Ghostscript (gsdll32.dll) | 功能最完整、支援 PDF 1.7+、字體渲染精準、命令列參數豐富 | 需部署原生 DLL、AGPL 授權 (商業用途需購買授權)、32/64 位元需對應 | AGPL / 商業授權 |
| Pdfium (Chrome PDF 引擎) | 渲染品質極高、BSD 授權、支援 .NET Standard | API 較低階、中文文檔少 | 免費開源 |
| PDFium.NET / PdfiumViewer | 封裝好用、NuGet 即裝即用 | 依賴 Pdfium 原生庫 | 免費 |
| iTextSharp (iText 5) | PDF 操作功能強 | 渲染轉圖非強項、AGPL | AGPL |
| 商業庫 (Aspose.PDF, Spire.PDF) | API 友善、支援完整、技術支援 | 昂貴 | 商業授權 |
gsdll32.dll (32-bit) / gsdll64.dll (64-bit) 至專案 bin 目錄或系統路徑。Prefer 32-bit 並用 32-bit DLL;否則用 64-bit。gswin64c.exe -q -dNOPAUSE -dBATCH -sDEVICE=jpeg -r300 -sOutputFile=out.jpg input.pdfusing System;
using System.Collections;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace PdfToImage
{
/// <summary>
/// Ghostscript API 封裝:PDF → 影像 (JPEG/PNG/TIFF 等)
/// 參考:https://www.hrangel.com.br/index.php/2006/12/04/converter-pdf-para-imagem-jpeg-em-c/
/// </summary>
public class PdfConvert : IDisposable
{
#region Ghostscript Native Imports
[DllImport("gsdll32.dll", EntryPoint = "gsapi_new_instance", CallingConvention = CallingConvention.StdCall)]
private static extern int GsapiNewInstance(out IntPtr pinstance, IntPtr callerHandle);
[DllImport("gsdll32.dll", EntryPoint = "gsapi_init_with_args", CallingConvention = CallingConvention.StdCall)]
private static extern int GsapiInitWithArgs(IntPtr instance, int argc, IntPtr argv);
[DllImport("gsdll32.dll", EntryPoint = "gsapi_exit", CallingConvention = CallingConvention.StdCall)]
private static extern int GsapiExit(IntPtr instance);
[DllImport("gsdll32.dll", EntryPoint = "gsapi_delete_instance", CallingConvention = CallingConvention.StdCall)]
private static extern void GsapiDeleteInstance(IntPtr instance);
#endregion
#region 可配置屬性
public string DeviceFormat { get; set; } = "jpeg"; // jpeg, png16m, pngalpha, tiff24nc ...
public int ResolutionX { get; set; } = 300;
public int ResolutionY { get; set; } = 300
public int JpegQuality { get; set; } = 90; // 1-100
public bool FitPage { get; set; } = false;
public int FirstPage { get; set; } = 1;
public int LastPage { get; set; } = int.MaxValue;
public int Width { get; set; } = 0; // 0 = 自動
public int Height { get; set; } = 0;
#endregion
private IntPtr _gsInstance = IntPtr.Zero;
private bool _disposed = false;
public PdfConvert() { }
/// <summary>轉換 PDF 指定頁碼範圍為影像檔案</summary>
/// <param name="inputPdfPath">來源 PDF 路徑</param>
/// <param name="outputPattern">輸出檔名模式,含 %d 代表頁碼,如 "output_page_%d.jpg"</param>
public void Convert(string inputPdfPath, string outputPattern)
{
if (!File.Exists(inputPdfPath))
throw new FileNotFoundException("找不到 PDF 檔案", inputPdfPath);
// 建立 Ghostscript 實例
int rc = GsapiNewInstance(out _gsInstance, IntPtr.Zero);
if (rc != 0) throw new InvalidOperationException($"Ghostscript 實例建立失敗,錯誤碼: {rc}");
try
{
string[] args = BuildArgs(inputPdfPath, outputPattern);
IntPtr[] argvPtrs = MarshalArgs(args);
rc = GsapiInitWithArgs(_gsInstance, args.Length, argvPtrs[0]);
if (rc != 0)
throw new InvalidOperationException($"Ghostscript 執行失敗,錯誤碼: {rc}。參數: {string.Join(" ", args)}");
}
finally
{
FreeArgs(argvPtrs);
GsapiExit(_gsInstance);
GsapiDeleteInstance(_gsInstance);
_gsInstance = IntPtr.Zero;
}
}
private string[] BuildArgs(string inputPdf, string outputPattern)
{
var list = new ArrayList
{
"pdf2img", // 虛擬程式名稱
"-dNOPAUSE", // 不暫停
"-dBATCH", // 批次模式
"-dQUIET", // 安靜模式
"-dNOPROMPT", // 不提示
"-dPARANOIDSAFER", // 安全模式
"-dMaxBitmap=500000000", // 大位圖緩衝
$"-dFirstPage={FirstPage}",
$"-dLastPage={LastPage}",
"-dAlignToPixels=0",
"-dGridFitTT=0",
$"-sDEVICE={DeviceFormat}",
"-dTextAlphaBits=4", // 文字反鋸齒
"-dGraphicsAlphaBits=4", // 圖形反鋸齒
$"-r{ResolutionX}x{ResolutionY}"
};
if (DeviceFormat.Equals("jpeg", StringComparison.OrdinalIgnoreCase) && JpegQuality > 0 && JpegQuality <= 100)
list.Add($"-dJPEGQ={JpegQuality}");
if (FitPage) list.Add("-dPDFFitPage");
if (Width > 0 && Height > 0) list.Add($"-g{Width}x{Height}");
// 輸出與輸入檔案必須放在最後
list.Add($"-sOutputFile={outputPattern}");
list.Add(inputPdf);
return (string[])list.ToArray(typeof(string));
}
private IntPtr[] MarshalArgs(string[] args)
{
var handles = new GCHandle[args.Length];
var ptrs = new IntPtr[args.Length];
for (int i = 0; i < args.Length; i++)
{
// 轉 ANSI 字節陣列 + 結尾 \0
byte[] ansi = Encoding.GetEncoding(1252).GetBytes(args[i] + '\0');
handles[i] = GCHandle.Alloc(ansi, GCHandleType.Pinned);
ptrs[i] = handles[i].AddrOfPinnedObject();
}
// 將指標陣列也 Pin 住
var argvHandle = GCHandle.Alloc(ptrs, GCHandleType.Pinned);
ptrs[0] = argvHandle.AddrOfPinnedObject(); // 第一個元素即為 argv 指標
// 將 handles 存入靜態欄位或傳回 Tuple 以便後續釋放,此處簡化略過
return ptrs;
}
private void FreeArgs(IntPtr[] argvPtrs)
{
// 實際專案需追蹤 GCHandle 陣列正確釋放,此處略
}
public void Dispose()
{
if (!_disposed && _gsInstance != IntPtr.Zero)
{
GsapiExit(_gsInstance);
GsapiDeleteInstance(_gsInstance);
_gsInstance = IntPtr.Zero;
_disposed = true;
}
}
}
}
public partial class PdfConvertPage : System.Web.UI.Page
{
protected async void btnConvert_Click(object sender, EventArgs e)
{
string pdfPath = Server.MapPath("~/Uploads/sample.pdf");
string outDir = Server.MapPath("~/Output/");
string pattern = Path.Combine(outDir, "page_%d.jpg");
if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir);
await Task.Run(() =>
{
using (var converter = new PdfConvert())
{
converter.ResolutionX = 200;
converter.ResolutionY = 200;
converter.JpegQuality = 85;
converter.Convert(pdfPath, pattern);
}
});
// 回傳生成的圖片清單給前端顯示
string[] files = Directory.GetFiles(outDir, "page_*.jpg");
rptImages.DataSource = files.Select(f => "~/Output/" + Path.GetFileName(f));
rptImages.DataBind();
}
}
[HttpPost("convert")]
public async Task ConvertPdf(IFormFile pdfFile, int dpi = 200, int quality = 85)
{
if (pdfFile == null || pdfFile.Length == 0) return BadRequest("No file");
var tempPdf = Path.GetTempFileName() + ".pdf";
var outDir = Path.Combine(Path.GetTempPath(), "pdf2img_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(outDir);
var pattern = Path.Combine(outDir, "page_%d.jpg");
try
{
using (var fs = File.Create(tempPdf))
await pdfFile.CopyToAsync(fs);
await Task.Run(() =>
{
using (var converter = new PdfConvert())
{
converter.ResolutionX = dpi;
converter.ResolutionY = dpi;
converter.JpegQuality = quality;
converter.DeviceFormat = "jpeg";
converter.Convert(tempPdf, pattern);
}
});
var files = Directory.GetFiles(outDir, "page_*.jpg");
// 打包 ZIP 回傳或上傳至 Blob Storage 回傳 URL 陣列
return Ok(new { pages = files.Length, message = "Conversion done" });
}
finally
{
// 清理暫存 (生產環境建議用背景工作)
if (File.Exists(tempPdf)) File.Delete(tempPdf);
if (Directory.Exists(outDir)) Directory.Delete(outDir, true);
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| DllNotFoundException: gsdll32.dll | DLL 未在 PATH 或 bin 目錄 | 複製對應位元數 DLL 至 bin;或設定 AppDomain.CurrentDomain.AppendPrivatePath |
| BadImageFormatException | 32/64 位元不匹配 | 專案屬性 → Build → 取消勾選 "Prefer 32-bit" (用 64-bit DLL) 或勾選 (用 32-bit DLL);IIS 應用程式集區「啟用 32 位元應用程式」需一致 |
| 轉換極慢 / 記憶體暴增 | 高解析度、大頁面、未限制 MaxBitmap | 降低 DPI (150-200 足夠螢幕顯示)、分頁轉換、設定 -dMaxBitmap |
| 中文字型缺字、亂碼 | Ghostscript 找不到字型 | 安裝中文字型 (Noto Sans CJK) 至系統;或指定 -sFONTPATH=/path/to/fonts |
| 多頁 PDF 僅輸出第一頁 | LastPage 預設值錯誤 | 明確設定 LastPage = int.MaxValue 或頁數 |
Ghostscript 採用 AGPL v3 授權。若您的應用程式透過網路提供服務 (如 SaaS、內部系統) 並呼叫 Ghostscript,屬於「傳播」行為,**必須開源完整應用程式原始碼**或購買 Artifex 商業授權。企業專案請務必確認法務風險,或改用 BSD/MIT 授權的 Pdfium 方案。
Ghostscript 是 PDF 轉影像功能最強大、參數最豐富的開源方案,透過 P/Invoke 封裝 gsdll32.dll 可在 .NET 中完整使用。本文提供生產級封裝類別、ASP.NET 同步/非同步呼叫範例、部署與授權關鍵點。實務上建議:低頻、內部、非商業專案可直接用;高併發、商業、雲端專案請評估 Pdfium 或商業庫,並將轉換作業移至背景佇列 (Hangfire, Azure Functions, AWS Lambda) 避免阻塞 Web 執行緒。