企業常需將大量 PDF 自動轉為圖片(如預覽、OCR 前處理)。本文示範建立 Windows Service,每 2 秒掃描指定資料夾,偵測新增/變更 PDF,呼叫 Adobe Acrobat COM 元件逐頁轉檔為 JPG,並透過剪貼簿中轉取得 Bitmap 存檔。完整包含服務架構、Installer、Setup 專案與 App.config 設定。
public partial class PDFConverter : ServiceBase
{
private System.Timers.Timer _timer;
public PDFConverter() { InitializeComponent(); }
protected override void OnStart(string[] args)
{
_timer = new System.Timers.Timer(2000); // 2 秒
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// 必須在 STA 執行緒呼叫 COM
var thread = new Thread(Process) { ApartmentState = ApartmentState.STA };
thread.Start();
}
protected override void OnStop() { _timer.Stop(); }
[STAThread]
private void Process()
{
string pdfDir = ConfigurationManager.AppSettings["pdfFilepath"];
string imgDir = ConfigurationManager.AppSettings["imageFilepath"];
if (!Directory.Exists(pdfDir)) return;
if (!Directory.Exists(imgDir)) Directory.CreateDirectory(imgDir);
var pdfFiles = new DirectoryInfo(pdfDir).GetFiles("*.pdf");
foreach (var file in pdfFiles)
{
string destPdf = Path.Combine(imgDir, file.Name);
// 僅處理新檔或已更新檔
if (File.Exists(destPdf) && File.GetCreationTime(destPdf) >= file.CreationTime)
continue;
// 備份剪貼簿
var oldData = Clipboard.GetDataObject();
File.Copy(file.FullName, destPdf, true);
var doc = new AcroPDDoc();
doc.Open(file.FullName);
int pageCount = doc.GetNumPages();
for (int i = 0; i < pageCount; i++)
{
var page = (AcroPDPage)doc.AcquirePage(i);
var point = (AcroPoint)page.GetSize();
var rect = new AcroRectClass
{
Left = 0, Top = 0,
right = point.x, bottom = point.y
};
try
{
if (page.CopyToClipboard(rect, 0, 0, 100)) // 100% 解析度
{
var pdfData = Clipboard.GetDataObject();
var bmp = (Bitmap)pdfData.GetData(DataFormats.Bitmap);
if (bmp != null)
{
string outPath = Path.Combine(imgDir,
$"{Path.GetFileNameWithoutExtension(file.Name)}_{i}.jpg");
bmp.Save(outPath, ImageFormat.Jpeg);
bmp.Dispose();
}
}
}
finally
{
Marshal.ReleaseComObject(page);
Marshal.ReleaseComObject(point);
}
}
Clipboard.Clear();
Marshal.ReleaseComObject(doc);
if (oldData != null) Clipboard.SetDataObject(oldData);
}
}
}
| 項目 | 說明 |
|---|---|
| STA 執行緒 | Acrobat COM 需單一執行緒單元,`Thread.ApartmentState = STA` + `[STAThread]` |
| 剪貼簿中轉 | `page.CopyToClipboard` → `Clipboard.GetDataObject().GetData(DataFormats.Bitmap)` |
| 解析度控制 | `CopyToClipboard` 最後參數 100 代表 100%,可調整品質/大小 |
| COM 物件釋放 | `Marshal.ReleaseComObject` 逐層釋放,避免 Acrobat 殭屍進程 |
| 剪貼簿還原 | 處理前備份、處理後還原,不干擾使用者操作 |
| 增量處理 | 比對 `CreationTime` 只轉新檔/異動檔 |
[RunInstaller(true)]
public partial class ProjectInstaller : Installer
{
public ProjectInstaller() { InitializeComponent(); }
private void ProjectInstaller_AfterInstall(object sender, InstallEventArgs e)
{
using (var rk = Registry.LocalMachine)
using (var sub = rk.CreateSubKey($@"SYSTEM\CurrentControlSet\Services\{serviceInstaller1.ServiceName}"))
{
int val = (int)sub.GetValue("Type", 0);
sub.SetValue("Type", val | 0x100); // 256 = SERVICE_INTERACTIVE_PROCESS
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="pdfFilepath" value="D:\PDFPath\" />
<add key="imageFilepath" value="D:\ImagePath\" />
</appSettings>
</configuration>
此方案在「已具 Acrobat Pro 授權、Windows Server 環境、低頻轉檔」場景下快速落地。若追求高併發、跨平台、無授權成本,請評估上述開源/商業庫替代。