首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

C# 使用 Ghostscript 將 PDF 轉換為高品質 JPG 圖片完整實作

     編輯:ASP.NET 基礎

方案選型對比

方案優點缺點授權/成本
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 友善、支援完整、技術支援 昂貴 商業授權

Ghostscript 部署與環境準備

  1. 下載 Ghostscript 安裝程式 (建議 9.56.1+)。
  2. 安裝時勾選「安裝 DLL 至 System32」或手動複製 gsdll32.dll (32-bit) / gsdll64.dll (64-bit) 至專案 bin 目錄或系統路徑。
  3. 專案設定 目標平台:若 IIS 應用程式集區啟用 32-bit,專案勾選 Prefer 32-bit 並用 32-bit DLL;否則用 64-bit。
  4. 測試命令列:gswin64c.exe -q -dNOPAUSE -dBATCH -sDEVICE=jpeg -r300 -sOutputFile=out.jpg input.pdf

完整 C# 封裝類別 (PDFConvert.cs)

using 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;
            }
        }
    }
}

ASP.NET Web Forms / MVC 控制器使用範例

Web Forms 非同步處理 (避免阻塞執行緒)

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();
    }
}

ASP.NET Core Controller 版本

[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 執行緒。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved