C#でPDFファイルを画像に変換するコードを書く

はじめに

PDFファイルを,画像ファイルに変換してみた.(OSはWindowsXP,開発環境はVisual studio8,言語はC#.)

どんなやり方があるか

PDFファイルを画像ファイルに変換する商用ライブラリはいくつか見つかる.Sourceforgeで探したところ,しかし,フリーでは見つからなかった.
考えられる方法には:

  • 仮想プリンタドライバ,Microsoft XPS Document Writer,を使いPDFファイルをXPSファイルに変換する.
  • GhostscriptなどのUNIXでPDFファイルを表示,印刷するのに使用しているプログラムを利用する.

がある.

最初のXPS Document Writerを使うとPDFファイル(に限らず印刷可能なファイルはすべて)をXPSファイルに変換できる.だが,プログラムから印刷を実行すると保存先ファイルの問い合わせダイアログが表示されてしまう.プログラムから保存先ファイル名を指定する方法は,Printing documents to Microsoft XPS Document Writer without user interaction – Feng Yuan (袁峰)およびhttp://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1732872&SiteID=1で議論されていて,それは,1)Windows VistaXPSプリンタドライバを流用して保存ファイル名が指定できるドライバを用意する,および 2)ダイアログをプログラムで捕まえて,ファイル名の設定などの操作を行う,の2方法があるようだ.ただしXPS仮想プリンタドライバの流用は,時々印刷ができないことがあるそうだ.2番目のファイル名を指定する方法は1つの選択肢だろう.

Adobe Acrobat readerなどを使わずに,Ghostscriptなど他のプログラムを利用する方法も1つの選択肢ではあるが,しかし,プログラムとフォントをインストールするためにディスク容量を数十Mbyteは消費するうえして適切な設定をせねばならない手間がかかり,すでにGhostscriptをインストールしている場合まで考慮せねばならないために,これは採用したくない.

仮想画像プリンタ・ドライバを利用する

ここではレジストリを通して画像データ出力フォルダなどの設定をおこなうオープンソースな仮想画像プリンタ・ドライバhttp://sourceforge.net/projects/imageprinterを使う.以下,このドライバがインストールされているとして話を進める.

ソースコードを末尾に掲載している.

想定する実行環境とファイル名などの指定

Main関数の,文字列pdf_filepathおよびoutput_folderに,PDFファイルおよび画像出力先フォルダをそれぞれ指定して,Visual Studioでコンソールプログラムとしてこれをコンパイルすればよい.Acrobat reader8.0があることを前提としている.もしも違うパスにあるときは,146行目の,psInfo.FileName = @"C:\Program Files\Adobe\Reader 8.0\Reader\AcroRd32.exe";にAcrobat readerの実行ファイルをフルパスで指定する.

プリンタ設定をレジストリに書き込む

プログラムは,まずレジストリに画像出力先フォルダなどを設定する.先のImagePrinterの設定は,レジストリ"HKEY_LOCAL_MACHINE\Software\ImagePrinter"以下にある.レジストリは,レジストリエディタを起動して(スタートメニューの,ファイル名を指定して実行に"regedit"と入力する)その値を確認できる.このうち"path"に画像ファイルの出力フォルダを設定する.

コードの工夫1 ~型情報を利用してレジストリ読み書き~

レジストリを1つ1つ読み書きするコードを書く手間を省くためにコードに工夫をしている.まずレジストリの値を保存するクラス(ValueObject)を作る.これのフィールドの変数名をレジストリのkeyと同じ文字列にしておく.例えばレジストリから値をValueObjectのフィールドそれぞれに読み込むコードは下記のように書ける.

foreach (System.Reflection.FieldInfo finfo in typeof(printer_configration).GetFields())
  finfo.SetValue(this, read_registry(finfo.Name));                    

これは,ValueObjectの型情報からフィールドの情報を取り出し,フィールドの変数名をkeyとしてレジストリから値を読み出してフィールドに設定している.こうすれば,レジストリのkeyをいちいち文字列で記述したり,読み書きするコードを手で書いたときによくあるデバグを困難にするミス,マジックワードのスペルミスや読み書きをし忘れる,を避けられる.

Acrobat readerのコマンド・オプション指定

次にacrobat readerを使いPDFファイルをImagePrinterに印刷する.Acrobat readerにオプション"/s /h /t ImagePrinter"を指定している.オプションの詳細はコマンドラインでPDFファイルを印刷する。(Acrobat Reader 6.0): Windows Script Programmingにある.オプションの"/s"は起動ウィンドウ抑止,"/h"が最小化ウィンドウ/印刷設定画面抑止,そして"/t"がプリンタ名の指定である.

プリンタの終了待ち

最後に印刷終了を待つ.これはまず出力先フォルダに画像ファイルができたことを検出してから,プリンタのジョブがなくなるまで待つことで実現した.プリンタのジョブ検出は,ここではまず,DOBON.NET プリンタのポート、状態を取得する - .NET Tips (VB.NET,C#...)にあるWIN32 APIを使うサンプルコードを利用している.
.net framework 2.0以降を利用しているならば,System.Printing名前空間を使い,以下のコードでプリンタのジョブ数が0になるのを待つことができる.このコードをコンパイルするにはプロジェクトの参照設定に"System.Printings"を追加する必要がある.

                      System.Printing.LocalPrintServer prtSrv = new System.Printing.LocalPrintServer();
            System.Printing.PrintQueue queue  = prtSrv.GetPrintQueue("ImagePrinter");
            do
            {
                System.Threading.Thread.Sleep(1000);   // 処理待ち,                
                queue.Refresh();// キューの最新情報を読み込み
            } while (queue.NumberOfJobs > 0);

ソースコード

ソースコードを下記に示す:

using System;
using System.Collections.Generic;
using System.Text;

using System.Runtime.InteropServices;
using System.ComponentModel;

namespace pdf2image
{
    /// <summary>
    /// PDFファイルを画像リストに変換
    /// 2008/07/12 Akihiro Uehara    
    /// 
    /// </summary>    

    // 初期設定
    // 1. プリンタドライバのインストール
    //      http://sourceforge.net/projects/imageprinter
    //      のvirtual image printerをインストール
    public class Program
    {
        #region プリンタ設定情報のValueObject
        /// <summary>
        /// プリンタ設定の読み込み/書き込み
        /// </summary>
        class printer_configration
        {
            public string ext_app;
            public string format;
            public string format_ext;
            public string multipage_tiff;
            public string one_file;
            public string original_name;
            public string path;
            public string q_jpg;
            public string ShowProgress;

            const string _registry_prefix = @"Software\ImagePrinter\";

            public printer_configration()
            { 
                // レジストリから読み込み
                ReadRegistry();
            }

            // レジストリに書き込み
            void write_registry(string key, string value)
            {
                if (null == value)
                    return;

                // "HKEY_LOCAL_MACHINE\Software\ImagePrinter\" に書き込む
                Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(_registry_prefix, true);
                if (null != regkey)
                {
                    regkey.SetValue(key, value);
                    regkey.Close();
                }
            }

            // レジストリから読み込み
            string read_registry(string key)
            {
                string val = string.Empty;
                // "HKEY_LOCAL_MACHINE\Software\ImagePrinter\" に書き込む
                Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(_registry_prefix, false);
                if (null != regkey)
                {
                    val = (string)regkey.GetValue(key);
                    regkey.Close();
                }
                
                return val;
            }

            /// <summary>
            /// レジストリから読みこむ
            /// </summary>
            public void ReadRegistry()
            {
                // レジストリからフィールドに1つずつ読み込むコードを地道に書くとこうなります
                /*
                ext_app             = read_registry("ext_app");
                format              = read_registry("format");
                format_ext          = read_registry("format_ext");
                multiplepage_tiff   = read_registry("multiplepage_tiff");
                one_file            = read_registry("one_file");
                original_name       = read_registry("original_name");
                path                = read_registry("path");
                q_jpg               = read_registry("q_jpg");
                ShowProgress        = read_registry("ShowProgress");
                */
                
                //さすがに面倒です.フィールドの変数名とレジストリのキー値を同じにしているので,
                //なので,このクラスの型情報からフィールドの情報を取得して,自動的に読み込ませます.
                foreach (System.Reflection.FieldInfo finfo in typeof(printer_configration).GetFields())
                    finfo.SetValue(this, read_registry(finfo.Name));
            }

            /// <summary>
            /// レジストリに書き込む
            /// </summary>
            public void WriteRegistry()
            {
                //読み込みと同様,型のフィールド情報を使い,フィールドをレジストリに反映させます
                foreach (System.Reflection.FieldInfo finfo in typeof(printer_configration).GetFields())
                    write_registry(finfo.Name, (string)finfo.GetValue(this));
            }
        }
        #endregion

        /// <summary>
        /// 画像ファイルの存在チェック.フォルダ内にファイル名がkeyphraseで始まるファイルがあるか?
        /// </summary>
        static bool imageFileExists(string folder, string keyphrase)
        {
            string [] files = System.IO.Directory.GetFiles(folder);
            return Array.Exists<string>(files, delegate(string item) 
            {
                string fname =System.IO.Path.GetFileName(item);
                return keyphrase != fname && fname.StartsWith(keyphrase); });
        }

        static void Main(string[] args)
        {
            string pdf_filepath = @"c:\tmp\test.pdf"; // 変換するPDFファイル
            string output_folder = @"c:\tmp\";

            // 現在のプリンタ設定を保存
            printer_configration prevCfg = new printer_configration();
            printer_configration curCfg  = new printer_configration();
            // プリンタの設定
            curCfg.path          = output_folder;  //画像出力フォルダ
            curCfg.ext_app       = @"";
            curCfg.format        = "png";       //フォーマットは,TIFFよりファイルサイズが1/2~1/3の,png.
            curCfg.one_file      = "false";
            curCfg.original_name = "true";      // PDFファイルのファイルを画像ファイル名に利用する
            curCfg.ShowProgress  = "false";     // プログレスバーは表示しない

            Console.WriteLine("プリンタを設定");
            curCfg.WriteRegistry();

            // PDFファイルを印刷
            Console.WriteLine("Acrobat readerを起動");
            System.Diagnostics.ProcessStartInfo psInfo = new System.Diagnostics.ProcessStartInfo();
            psInfo.FileName = @"C:\Program Files\Adobe\Reader 8.0\Reader\AcroRd32.exe";
            psInfo.Arguments = String.Format(@" /s /h /t {0} ImagePrinter",pdf_filepath); // ファイル名は適切なPDFファイルを指定,オプション詳細は http://scripting.cocolog-nifty.com/blog/2006/12/pdf_4c95.html を参照.
            psInfo.CreateNoWindow = true; // コンソールを開かない
            psInfo.UseShellExecute = false; // シェルを使用しない
            System.Diagnostics.Process ps = new System.Diagnostics.Process();
            ps.StartInfo = psInfo;
            ps.Start();            

            // 画像ファイルの出力完了まち
            // まず,最初の画像ファイルが出来上がるのを待つ
            Console.WriteLine("画像ファイルを確認");
            string fname = System.IO.Path.GetFileName(pdf_filepath);
            while (! imageFileExists(output_folder, fname))
                System.Threading.Thread.Sleep(1000);
            
            // つぎに,プリンタジョブがなくなるのを確認
            Console.WriteLine("ジョブ完了を確認");
            PRINTER_INFO_2 pinfo;
            do
            {
                System.Threading.Thread.Sleep(1000);   // 処理待ち,
               pinfo = GetPrinterInfo("ImagePrinter"); // プリンタのジョブ数をポーリング
            } while (pinfo.cJobs > 0);
            /* .net framework 2.0 以降を使っているならば,同じことをSystem.Printing名前空間を利用して書ける.
               このコードをコンパイルするために,Visual Studioのプロジェクトの参照設定に"System.Printing"を追加する必要がある.
            System.Printing.LocalPrintServer prtSrv = new System.Printing.LocalPrintServer();
            System.Printing.PrintQueue queue  = prtSrv.GetPrintQueue("ImagePrinter");
            do
            {
                System.Threading.Thread.Sleep(1000);   // 処理待ち,                
                queue.Refresh();// キューの最新情報を読み込み
            } while (queue.NumberOfJobs > 0);
*/           
            //ps.Kill(); // Acrobat reader強制終了

            // プリンタ設定を元に戻す
            Console.WriteLine("プリンタ設定の復元");
            prevCfg.WriteRegistry();
        }

        #region DOBON.NET http://dobon.net/vb/dotnet/graphics/printerport.html プリンタのポート、状態を取得する, 参照

        //using System.Runtime.InteropServices;
        //using System.ComponentModel;

        [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool OpenPrinter(string pPrinterName,
            out IntPtr hPrinter, IntPtr pDefault);

        [DllImport("winspool.drv", SetLastError = true)]
        private static extern bool ClosePrinter(IntPtr hPrinter);

        [DllImport("winspool.drv", SetLastError = true)]
        private static extern bool GetPrinter(IntPtr hPrinter,
            int dwLevel, IntPtr pPrinter, int cbBuf, out int pcbNeeded);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct PRINTER_INFO_2
        {
            public string pServerName;
            public string pPrinterName;
            public string pShareName;
            public string pPortName;
            public string pDriverName;
            public string pComment;
            public string pLocation;
            public IntPtr pDevMode;
            public string pSepFile;
            public string pPrintProcessor;
            public string pDatatype;
            public string pParameters;
            public IntPtr pSecurityDescriptor;
            public uint Attributes;
            public uint Priority;
            public uint DefaultPriority;
            public uint StartTime;
            public uint UntilTime;
            public uint Status;
            public uint cJobs;
            public uint AveragePPM;
        }

        /// <summary>
        /// プリンタの情報をPRINTER_INFO_2で取得する
        /// </summary>
        /// <param name="printerName">プリンタ名</param>
        /// <returns>プリンタの情報</returns>
        public static PRINTER_INFO_2 GetPrinterInfo(string printerName)
        {
            //プリンタのハンドルを取得する
            IntPtr hPrinter;
            if (!OpenPrinter(printerName, out hPrinter, IntPtr.Zero))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            IntPtr pPrinterInfo = IntPtr.Zero;
            try
            {
                //必要なバイト数を取得する
                int needed;
                GetPrinter(hPrinter, 2, IntPtr.Zero, 0, out needed);
                if (needed <= 0)
                    throw new Exception("失敗しました。");

                //メモリを割り当てる
                pPrinterInfo = Marshal.AllocHGlobal(needed);

                //プリンタ情報を取得する
                int temp;
                if (!GetPrinter(hPrinter, 2, pPrinterInfo, needed, out temp))
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                //PRINTER_INFO_2型にマーシャリングする
                PRINTER_INFO_2 printerInfo =
                    (PRINTER_INFO_2)Marshal.PtrToStructure(pPrinterInfo,
                    typeof(PRINTER_INFO_2));

                //結果を返す
                return printerInfo;
            }
            finally
            {
                //後始末をする
                ClosePrinter(hPrinter);
                Marshal.FreeHGlobal(pPrinterInfo);
            }
        }
        #endregion
    }    
}