2010年05月24日

Windows仮想プリンタプログラムを作ってみる

はてなブックマークに登録

普段よく使っているソフトウェアであっても、どういうしくみで機能を実現しているのかよくわからないものが結構あります。 筆者は主に Windows 環境で作業をしていますが、PDF ファイルを作成するたびに目にする「仮想プリンタ」もそのひとつでした。

システムは本物のプリンタだと信じているのに実はそれはソフトウェアへのインターフェイスにすぎず、印刷ジョブを渡したら最後、データは隅から隅までなめまわされ好きなように処理されてしまう。ということは、その気になればあんなことやこんなこともできてしまうはず・・・。 あらためて考えてみるとなかなか面白い話なので、仮想プリンタのしくみを調べて何かプログラムを書いてみたいと思いました。

手はじめに、定番の題材として所定のドキュメントを PDF や画像に変換しファイル出力する仮想プリンタを作ってみることにしました。 ひとつの仮想プリンタを自作のコードで構築し、データ出力部分を著名なオープンソースソフトウェアの GhostscriptRedMonImageMagick で処理する内容です。

作成したプログラムとそのソースコードをフリーソフトウェアとして公開します。 こういった具体的な開発情報は案外見あたりませんので興味のあるかたはご覧下さい。
MyVirtualPrinter - github.com/mkttanabe
プログラム開発には無償で利用できる以下のソフトウェアを使用しました。

  • Microsoft Visual C++ 2008 Express Edition (*)
  • ResEdit (*)

今回はこの MyVirtualPrinter を切り口に、Windows での印刷処理の概要と仮想プリンタをプログラムで構成する方法について説明します。

1. 動作環境

MyVirtualPrinter.exe と仮想プリンタ「MyVirtualPrinter」の動作は筆者の手元にある次の 32bit Windows 環境で確認しています。

  • Windows XP Home SP3
  • Windows XP Professional SP3
  • Windows Vista Business SP2
  • Windows 7 Professional
記事中の操作画像はすべて Windows XP 環境で採取したものです。

2. 使い方

MyVirtualPrinter の使い方はとても簡単です。ダウンロードしたアーカイブを適当なフォルダに展開して下さい。
MyVirtualPrinter 下の「MyVirtualPrinter.exe」を実行すると次のパネルが開きます。

「仮想プリンタの登録」がチェックされた状態で「実行」ボタンを押下すると、MyVirtualPrinter という名前の仮想プリンタがシステムに登録されます。

この仮想プリンタは、印刷処理の過程でアーカイブを展開したフォルダを参照します。 そのため、ふたたび MyVirtualPrinter.exe を実行してこの仮想プリンタをシステムから削除するまでは当該フォルダを削除しないで下さい。

任意のアプリケーションからの印刷時にプリンタとして「MyVirtualPrinter」を選択します

以下のダイアログで出力ファイル名と種類を指定して「保存」を押下するとその形式でファイル出力されます

登録ずみの MyVirtualPrinter をシステムから削除するにはふたたび MyVirtualPrinter.exe を実行して下さい



3. 外部リソース

MyVirtualPrinter は、次のオープンソースソフトウェア(以下 OSS)プロジェクトに含まれるビルドずみのモジュールを使用しています。

  • GPL Ghostscript (C) Artifex Software, Inc
    gswin32c.exe, gsdll32.dll
  • RedMon (C) Ghostgum Software Pty Ltd.
    redmonnt.dll
  • ImageMagick (C) ImageMagick Studio LLC
    convert.exe
MyVirtualPrinter のアーカイブにはこれらのモジュールと各ソフトウェアのライセンス規約が含まれます。ご確認下さい。

4. Windows での印刷のしくみ

Windows 上でプリンタとして表現されるオブジェクト(物理装置としての「プリンタ」ではなく)を構成する要素とプリントジョブの処理の流れは複雑です。下の図はその内容をごく大まかに要約したものです。

  • プリンタドライバ
    ユーザモード空間で動作するドライバで実体は DLL。通常、ユーザインターフェイスを備えた設定用のモジュールと、印刷データの描画やプリンタ用のコマンドの処理を行うモジュールに分れる
  • プリントスプーラサービス
    プリントジョブのキューイングとディスパッチを司る
  • プリントプロセッサ
    スプーラから呼び出され、ジョブデータの変換や一時停止・中止・再開等の制御を司る
  • ポートモニタ
    スプーラから呼び出され、印刷データを所定のポートへ出力する
  • ポート
    所定のプリンタとデータを授受するためのインターフェイス
仮想プリンタを作る場合にもこれと同様の要素を構成すればよいのです。これらはすべてソフトウェアの世界のことですから、それぞれが所定の規約に適合していれば、それが紙へ印字する装置への出力を行うためのプリンタ構成であってもそうでなくても Windows はひとしく「プリンタ」と認識するわけですね。

5. MyVirtualPrinter の構成

試作した MyVirtualPrinter の構成を示します。

  • MyVirtualPrinter.exe
    各ソフトウェア要素を規約に準じて構成し仮想プリンタとしてシステムへの登録(削除)を行う
  • pscript5.dll, ps5ui.dll
    Windows 標準の PostScript プリンタドライバ
  • MyVirtualPrinter.ppd
    MyVirtualPrinter 用の PostScript プリンタ定義体
  • RedMon
    プリンタへの出力に代えて所定のプログラムにプリントデータをリダイレクトするポートモニタプログラム (RedMonNt.dll)
  • RedMonProxy.exe
    RedMonNt ポートモニタの出力をログオンユーザ空間上の所定のプログラムで処理するためのインターフェイスモジュール
  • Ghostscript
    PostScript インタープリタ (gswin32c.exe, gsdll32.dll)
  • ImageMagick
    画像変換モジュール (convert.exe)

MyVirtualPrinter では Windows 標準の PostScript プリンタドライバを使用しています。本来このドライバは PostScript プリンタへの出力のために Microsoft 社 と Adobe 社が共同で開発・提供しているものですが、もちろん自作のプログラムから利用することもできます。

一般に Windows 上でプリンタドライバを開発するためには大きく二つの選択枝があります。ひとつは自分で最初から独自のドライバを作るという選択。もうひとつは、 Windows 標準の 汎用プリンタドライバを利用することです。 汎用プリンタドライバは印刷処理に必要となる最大公約数的な実装から構成されており、開発者は対象とするプリンタに応じてここに必要な機能を追加することができます(ミニプリンタドライバ)。Windows の PostScript プリンタドライバはそのひとつです。

PostScript プリンタドライバを選んだのは試作の題材を PDF 変換としたためです。PostScript プリンタドライバの出力する印刷データは PostScript コードそのものですから、OSS の優れた PostScript インタープリタである Ghostscript でこれを処理すればもっとも手早く PDF 化を実現することができると考えました。画像変換については、有名な ImageMagick が Ghostscript と非常に相性が良さそうです。 また、印刷データをプリンタではなくソフトウェアに振り向けることのできる仮想ポートモニタ RedMon をここに組み合わせることにしました。

以上が MyVirtualPrinter の印刷処理部分の大枠です。理屈がわかってしまえば簡単ですね。 問題は、それぞれのソフトウェア要素をひとつの仮想プリンタとして構成するにはどうすればよいか?という点です。

6. MyVirtualPrinter.exe の処理内容

MyVirtualPrinter.exe は仮想プリンタの構成と削除を行うためのプログラムです。 このプログラムでは以下の処理を行っています。詳細はソースコードを参照して下さい。

  1. プリンタ「MyVirtualPrinter」の存在チェック
    システム上の MyVirtualPrinter の有無を調べ、未登録なら登録処理、登録ずみなら削除処理へ移行。(以下の説明は登録処理)

  2. ポートモニタの登録
    RedMonNt.dll が WINDIRsystem32 に存在しなければコピーし、Win32API AddMoniter により「MyVirtualPrinter Redirected Port」という名前のポートモニタとしてシステムへ登録する。

    この情報は次のレジストリキー配下に格納される:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors

  3. ポートの作成
    OpenPrinter API, XcvData API を使用して、 登録ずみの「MyVirtualPrinter Redirected Port」の実体である RedMonNt.dll の標準ポートモニタインターフェイス経由で専用ポート「MyVirtualPrinterPort:」を作成する。あわせて、RedMon 固有のレジストリ項目に、データリダイレクト先のプログラムとして「RedMonProxy.exe」を登録する。

    この情報は次のレジストリキー配下に格納される:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\MyVirtualPrinter Redirected Port\Ports

  4. プリンタドライバの登録
    MyVirtualPrinter の PostScript プリンタ定義体「MyVirtualPrinter.ppd」をシステムのプリンタドライバフォルダへコピーし、 標準 PostScript プリンタドライバの「pscript5.dll」「ps5ui.dll」との組み合わせを AddPrinterDriver API により「MyVirtualPrinterDriver」の名前でシステムに登録する。
    ただし、システムに一度も PostScript プリンタドライバがインストールされた実績がなければ「pscript5.dll」「ps5ui.dll」は上記のドライバディレクトリに存在しないため、その場合は Windows の保持するセットアップ用データから両ファイルを抽出して導入する

    この情報は次のレジストリキー配下に格納される:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Environments\Windows NT x86\Drivers\Version-3

  5. プリンタの登録
    AddPrinter API により、プリンタ「MyVirtualPrinter」をシステムへ登録する。ここで上記の処理において登録ずみのポート名・プリンタドライバ名および標準のプリントプロセッサである「WinPrint」を指定することにより、この仮想プリンタの構成要素がシステムに提示される。

    この情報は次のレジストリキー配下に格納される:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Printers

MyVirtualPrinter の削除は逆の順番で処理しています。
  1. プリンタエントリの削除
  2. プリンタドライバエントリの削除
  3. ポートエントリの削除
  4. ポートモニタエントリの削除

7. RedMonProxy.exe とは

RedMonProxy.exe は RedMon ポートモニタから呼び出されるプログラムです。 このプログラムは次の処理を行います。

  1. 子プロセスを起動(親プロセスの処理)
    現在のログオンユーザのセキュリティコンテキストで自分自身の新しいプロセスを作成する。
  2. プログレスバーを表示(子プロセスの処理)
    処理が進行中であることをユーザに知らせるため。
  3. プリントデータを一時ファイルへ(親プロセスの処理)
    RedMon から渡されたプリントデータ(PostScript)を一時ファイルへ出力〜完了時に子プロセスへ通知。
  4. 出力ファイル名+形式をユーザに確認(子プロセスの処理)
    出力ファイル名問い合わせ用のコモンダイアログを表示。
  5. 所定の形式に変換してファイル出力(子プロセスの処理)
    指定された出力形式に応じて Ghostscript, ImageMagick モジュールを呼び出し変換を実行して自プロセスを終了。
  6. 一時ファイルを削除(親プロセスの処理)
    子プロセスの処理完了を待ち PostScript 一時ファイルを削除して自プロセスを終了。
要するに RedMon から PostScript データを受け取って Ghostscript や ImageMagick に引き渡しているわけですが、自分自身の処理をふたつのプロセスに分けていることには意味があります。これについて説明します。

RedMonProxy.exe を呼び出すのは RedMon ポートモニタのコードで、そのコードを実行するのは Windows のプリントスプーラサービス(Spooler)です。Windows のシステムサービスは通常ローカル システム アカウント(LocalSystem account)で実行されています。
つまり、RedMonProxy.exe はシステムサービスの中から起動されるということです。 サービス側で意図的に設定しない限り、通常はサービスの中からログオンユーザのデスクトップに UI を表示することはできませんし、SYSTEM アカウントからログオンユーザ環境にファイルを出力することは権限管理の観点からもバランスがよくありません。

実はこうした問題に対応するために RedMon には「RunAsUser」というオプションが用意されています。このオプションを有効にすると対象プログラムを現在のログオンユーザのセキュリティコンテキストで実行することができるようになっています。 これで万事解決かとも思ったのですが、試してみると微妙に動きのおかしいところがありました。Sysinternals の Process Explorer でプロセス情報を確認したところ、新しいプロセスにはログオンユーザの環境ブロックが反映されず、呼び出し側プロセスのそれが継承されていることがわかりました。

このことは、たとえばこの状態で起動されたプロセスの中から %TEMP% を参照すると、ログオンユーザに本来割り当てられたテンポラリフォルダではなく、サービスを実行しているローカル システム アカウントの環境での同ロケーションが示されることを意味します。 RedMon 正式版 1.7 のリリースは 2001 年とふるく現在もメインストリームの開発が継続されているのか否か定かではありませんが今後何らかの形でフィードバックできればと思います。

この件は RedMon の当該箇所に手を加えるのではなく RedMonProxy.exe 内部で処理することにしました。 以前にもサービスプロセスから UI を持つプログラムをログオンユーザのセキュリティコンテキストで実行したいケースがあったためです。 関連する話題として Windows Vista, 7 には「セッション 0 の分離」といういささか悩ましい仕様変更があります。ここではその詳細には触れませんが、RedMonProxy 親子プロセス間で同期用のカーネルオブジェクトを共有するというごく平凡な処理においてこのことの影響を受けました。

8. プログラムのビルドについて

MyVirtualPrinter.exe と RedMonProxy.exe のソースコードをダウンロードアーカイブの src/ フォルダ下に収めています。記事の冒頭で触れたとおり、いずれも Microsoft Visual C++ 2008 Express Edition で開発を行ったものです。 他の VC++ 環境でのビルドは試していませんが、アーカイブに含まれるプロジェクト構成ファイルがお手持ちの VC++ 環境と互換性のない場合は下記の方法を試して下さい。

  • 新規 Win32 プロジェクトを作成し *.cpp, *.h, *.rc をツリーに加える
  • プロジェクトの文字セットに Unicode を指定する
  • ランタイムライブラリとして /MT(リリース), /MTd(デバッグ)を指定する
  • マニフェストファイル(rt_manif.bin)を適切にリンクする

9. 注意事項

  • MyVirtualPrinter.exe とそのソースコード、ならびに RedMonProxy.exe とそのソースコードに関する全ての著作権はKLab株式会社に帰属します。
  • MyVirtualPrinter.exe とそのソースコード、ならびに RedMonProxy.exe とそのソースコードは、GNU General Public License (GPL) に準ずるフリーソフトウェアです。個人的に使用する場合は、改変・複製に制限はありません。配布する場合は GPL に従って下さい。また、Aladdin Free Public License のもとに公開されている RedMon との併用を許可します。
  • GPL Ghostscript は Artifex Software, Inc が権利を保有するソフトウェアです。
    RedMon は Ghostgum Software Pty Ltd. が権利を保有するソフトウェアです。
    ImageMagick は ImageMagick Studio LLC が権利を保有するソフトウェアです。
    これらのソフトウェアを使用・配布する際にはそれぞれのライセンス規約を遵守して下さい。
  • MyVirtualPrinter.exe とそのソースコード、ならびに RedMonProxy.exe とそのソースコードは無保証です。それらを使って生じたいかなる損害に対してもKLab株式会社は責任を負いません。詳しくは GPL を参照して下さい。

(tanabe)
参考文献: Interface 2008年 8月号 CQ出版社刊
klab_gijutsu2 at 11:27│Comments(21)win 

この記事へのコメント

1. Posted by p   2010年05月25日 13:35
非常にためになりました。今後の勉強の素材として利用させて頂きます。
有難うございました。
2. Posted by tanabe   2010年05月25日 13:43
pさん、ご丁寧なコメントをありがとうございます。お役に立てば何より幸いです :-)
3. Posted by zono   2010年07月04日 19:29
4 大変勉強になりました!
ところで、MyVirtualPrinter ver 1.0.0.2
をダウンロードしようとすると、必ず67%あたりで終了し
壊れたzipファイルになってしまうのですが
自分だけでしょうか?
4. Posted by tanabe   2010年07月04日 19:48
zonoさん、コメントをありがとうございます。
サーバ上のアーカイブに問題はないようです。ブラウザのキャッシュの影響が考えられますので、別のブラウザを使うか、あるいはダウンロードURLの末尾に http://.../MyVirtualPrinter_1002.zip?hogehogeの要領で「?」に続けて適当なダミー文字列を連結して試してみて下さい。
5. Posted by zono   2010年07月04日 21:19
素早い返答ありがとうございます。
?文字以降のダミー文字、およびキャッシュを消してからダウンロードを試みてみましたが、
同じくダウンロードに失敗してしまいました。

ブラウザはIE7とFireFoxです。
Irvineというダウンローダーでも試してみましたが、同様の結果になりました。
6. Posted by tanabe   2010年07月04日 22:15
zonoさん、原因がわかりませんが、念のため別の名前でアーカイブを再アップロードしました。このページをリロードして試してみて下さい。もしこれでも×な場合はメールアドレス(公開しません)を添えてお知らせ下さい。
7. Posted by zono   2010年07月04日 22:41
ダウンロードに成功しました。
素早い対応、本当にありがとうございます。
8. Posted by tanabe   2010年07月04日 22:51
zonoさん、ご連絡ありがとうございます。結果をお聞きして安心しました。
9. Posted by black   2010年08月02日 20:15
DLリンク無効になってます。

MyVirtualPrinter_1002.zip
の名前でDLできたので
MVP1002.zip
から変更しておいた方がいいですよ。

それから、ソースコードの公開うれしいです。
勉強させていただきます。
10. Posted by tanabe   2010年08月02日 22:03
blackさん、コメントをありがとうございます。
本ブログはlivedoor社のブログサービスを利用して発信しているのですが、ごく最近、同サービスにおいて扱えるファイルサイズの仕様に変更があったようです。とりあえず別の場所にコピーを置きコピーへのリンクを併記しました。ご指摘に感謝します。
11. Posted by hamada   2010年10月25日 16:58
非常に勉強になります。 
ソースまで公開していただき、重ね重ねありがとうございました。
12. Posted by tanabe   2010年10月25日 17:07
hamadaさん、コメントをありがとうございます。ご参考になれば幸いです。
13. Posted by kent   2011年06月10日 11:56
こんにちは。

非常に有用なサンプルコードをありがとうございます。

 動かしてみて気付いたのですが、ユーザーにファイル名を入力させるダイアログをスキップ(特定ファイル出力とする)した場合、空白のページが出力される場合があるようです。
 デバッグで追っていくと、問題ないことからタイミング問題ではないかと思いました。
 もしかしたら、ロックがうまくいってなくて、親プロセスによる一時ファイル出力(PostScriptファイル)が終わってなくても子プロセスの出力待ち処理以降の処理をやってしまっているのかな?と思いました。

 とりあえず、調べてる途中ですが報告します。
14. Posted by tanabe   2011年06月10日 12:10
kentさん、興味ぶかいご指摘をありがとうございます。
残念ながら私にはいま調査を行うための時間がありませんが、ぜひお手元で実験・改良なさって下さい。まずは良い成果をお祈りします。
15. Posted by lee   2014年05月15日 11:20
kentさんが発見された事象はこちらでも発生しました。原因も分かった?教えてくれませんか。
16. Posted by fumi   2015年05月21日 16:51
kentさんが発見された事象ですが、
こちらでは「本 exe の新しいプロセスを起動」
の前にResetEventでシグナルを強制Off
させる事で解決しましたよ。

有用なサンプルコードをありがとうございます。

17. Posted by kkk   2018年07月26日 13:05
サンプルコードはどこでDLできますでしょうか?
18. Posted by tanabe   2018年07月27日 07:50
公開しているリソース一式へのリンクは記事冒頭近くの「ダウンロード」という箇所に掲載しています。「※ダウンロードできないときはこちらから」のリンクもお試し下さい。

19. Posted by sugi   2018年10月18日 07:09
このページの情報を参考に、自前の仮想プリンタを作成しようとチャレンジしています。貴重な情報の公開、ありがとうございます。

リソースのダウンロードについて質問です。
「ダウンロード」からダウンロードできる「MVP1002.zip」にはソースが含まれておらず、「※ダウンロードできないときはこちらから」のリンクは404エラーとなってしまいます。現状、ソースのご提供はされていないのでしょうか。
20. Posted by tanabe   2018年10月18日 11:19
sugi さん、コメントをありがとうございます。手元では代替リンクに問題はみられませんでしたが、この機に一式を GitHub へ移動し記事を更新しました。良い展開をお祈りします。
21. Posted by sugi   2018年10月19日 08:26
5 tanabeさん、無事、ソースコードをダウンロードできました。ありがとうございました。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント