CodeZine(コードジン)

特集ページ一覧

カスタムプリントプロセッサで印刷機能を強化する

DDKのサンプルを利用したプリントプロセッサの自作

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/11/22 12:00

カスタムプリントプロセッサは、印刷処理を制御するための方法の1つです。この記事では、プリントプロセッサのユーザーモードDLLとC/C++を使って、セキュリティポリシーの強化、足りない機能の追加、または印刷ジョブの変更を行う方法を紹介します。

はじめに

 次々に新しい技術が生み出され、超小型で非常に便利な最新の電子機器が続々と登場するこの世界で、プリンタは依然として重宝されています。人々は、まだ完全にソフトコピー(文書を電子的に複写したもの。PCなどの画面上で閲覧する)には移行していないのです。PCの画面をスクロールさせる代わりに、印刷された書類に目を通す理由はたくさんあります。たとえば、テクニカルライターの中には、文書の推敲にあたって、以前の内容のハードコピー(文書を紙に印刷したもの)にコメントを書き加えることにこだわる人がいます。

 理由はどうあれ、人々は頻繁に印刷を行っています。多くの場合、印刷作業を調整したり、その場で印刷ジョブを変更したりすることが必要になります。こうした印刷処理の制御を行うには、多くの方法があります。その1つが、カスタムプリントプロセッサの利用です。

印刷ジョブのライフサイクル

 図1は、印刷ジョブが通過する主要な段階――Windowsアプリケーションからの印刷要求に始まり、実際の印刷に至るまで――を表したものです。

 ユーザーが文書をプリンタに送信することによって、この処理が開始されます。Windowsが導入された企業の環境では、通常、アプリケーションによって印刷ジョブがプリントサーバーに送られます。ネットワーク上のプリンタは、このプリントサーバーによって共有されています。Windowsの印刷スプーラサービスが印刷要求を受け取り、そのジョブをディスク上にスプールします。次に、プリンタに関連付けられたプリントプロセッサが呼び出され、印刷ジョブをプリンタ言語に変換します。その後、変換されたデータストリームを適切なプリンタポート(COM、LPTなど)に振り向けるという追加のタスクをプリントモニタが実行します。この実行にはCreateFileWriteFileReadFile、およびDeviceIOControlというWin32 API関数が使われます。最後に、プリンタによって印刷された紙をユーザーが受け取るというわけです。

図1 印刷処理過程の概要: この図は、印刷ジョブが通過する各段階を表している
図1 印刷処理過程の概要: この図は、印刷ジョブが通過する各段階を表している

プリントプロセッサとは?

 プリントプロセッサは、その名のとおり、プリンタにスプールされるジョブを処理します。プリントプロセッサは、Windowsアプリケーションとプリンタの間のレイヤと見なすことができます。このレイヤは、サポートされるデータタイプ(NT EMF、RAW、TEXTなど)からのジョブストリームを、適切なドライバを利用して特定のプリンタ言語に変換します。どんなプリンタにも、スプールされる印刷ジョブの処理を受け持つプリントプロセッサが割り当てられています。またプリントプロセッサは、印刷の中止、一時停止、再開のためのあらゆる要求の処理も担当します。

 実は、プリントプロセッサはユーザーモードのDLLであり、Windowsの印刷スプーラサービスによって実行されます。Microsoftは、Driver Development Kit(DDK)の一部として、ドライバではないものの、サンプルコードを提供しています。このため、自作のプリントプロセッサを実装するのにドライバ開発の知識は必要ありませんが、コードの本質的な意味を理解するにはC/C++とWin 32のプログラミング経験が必要なことをご了承ください。

エクスポートされる関数

 ここでは、Windows印刷スプーラとプリントプロセッサDLLの間のインターフェイスを紹介します。表1は、エクスポートが必要な6つの関数とその役割を説明するリストです。

表1 エクスポートされるプリントプロセッサ関数
関数名説明
EnumPrintProcessorDatatypesプリントプロセッサがサポートするデータの型を取得
GetPrintProcessorCapabilities指定されたデータ型についてのプリントプロセッサの処理能力を取得
OpenPrintProcessor印刷セッション開始時にプリントプロセッサをオープンし、そのハンドルを取得
PrintDocumentOnPrintProcessorスプールされた印刷ジョブをプリンタ言語に変換
ControlPrintProcessor印刷の一時停止/中止/再開のための制御要求をプリントプロセッサに送信
ClosePrintProcessor印刷セッション終了時にプリントプロセッサをクローズ

未知の領域:プリントプロセッサで何ができるか?

 すべての印刷ジョブは必ずプリントプロセッサを経由するため、開発者は印刷処理をほぼ無制限に制御できます。先ほど説明したとおり、すべてのプリントプロセッサは、サポートされるデータ型をWindowsスプーラに通知する必要があります。各データ型は別々に処理され、制御のレベルはそれぞれ異なっています。たとえば、RAWデータ型はプリンタドライバ固有の形式でデータを表現します。RAWデータ型の印刷ジョブのコンテンツを調べるには、プリンタに特有のハンドラを書く必要があります。そのため、サポートされるプリンタの種類が制限されます。

 幸いなことに、ほとんどのWindowsアプリケーションは、印刷ジョブをNT EMF(デバイス非依存の拡張メタファイル形式)でスプールします。EMFは文書化されており、印刷される実際のデータの確認が容易です。プリントプロセッサは、Windows GDIのAPIを利用してプリンタのコンテキストでEMFレコードを読み取ることによって、EMF形式のジョブをプリンタ言語に変換します。自作のプリントプロセッサを開発する場合は、GDI関数を自由に呼び出して印刷ジョブを変更できます。

 プリントプロセッサは、あらゆる印刷ジョブの処理を、印刷ジョブを発行したNT/2000/2003ドメインアカウントのセキュリティのコンテキストを利用して行います。これは、コードの中でユーザーを識別したり、任意のセキュリティポリシーを強化できることを意味します。たとえば、Exchangeメールボックスのサイズが大きすぎるユーザーの印刷ジョブをブロックすることができます。プリントプロセッサは、セキュリティの点で非常に役に立ちます。単純にアクセスコントロールリストを定義するだけでは、このような複雑なパーミッションをプリンタに設定できないからです。

コーディングを始めるために必要なこと

 まず、開発環境を構築する必要があります。

  • Microsoft Visual Studio 7のインストール
  • Windows DDK 2000(またはそれ以降のバージョン)のインストール

 両製品とも、MSDNサブスクリプションのCD/DVDパッケージから入手できます。

 プリントプロセッサDLLの開発は、DDKビルドツールをコマンドラインから利用するか、Visual Studio 6またはVisulal Studio 7のようなIDE(統合開発環境)を利用するかのどちらかの方法で行います。この記事のサンプルは、Visual Studio 7を利用して開発されたものです。

自作プリントプロセッサの実装方法

 Microsoft DDKのサンプルを利用すれば、プリントプロセッサの自作は非常に容易です。この記事で紹介するコードは、DDKのgenprintをベースにしたものです。このサンプルはC言語で記述されていましたが、ここでは、オブジェクト指向モデルのメリットを活かすためにC++に書き換えてあります。

 この記事で紹介するプリントプロセッサのサンプルは、カスタマイズ可能なコンテンツを持つセパレータページを印刷ジョブごとに追加で印刷するものです。コンテンツとして、1000x1000のビットマップ画像ファイル(C:\SeparatorPP\Separator.bmp)を使います。この画像ファイルを入れ替えれば、好みの画像をこのプリントプロセッサで使用できます。

 それでは、サンプルのソースコードをじっくり見ながらその働きを理解していきましょう。表2は、ソースファイルの一覧です。それぞれに簡単な説明と、次のどのタイプにあてはまるかを記しています。

  • DDK -- Microsoft DDKのサンプルをそのまま利用したファイルです。
  • DDKを修正 -- Microsoft DDKサンプルのファイルを修正したものです。
  • 新規作成 -- 新たにスクラッチから作成したファイルです。
表2 サンプルのソースファイルの一覧。各ファイルの簡単な説明と、基になったファイルを示す
ファイル名説明タイプ
SeparatorPP.def定義ファイルをエクスポートDDK(ファイル名を「winprint.def」から変更)
SeparatorPP.cppあらゆるプリントプロセッサがエクスポートすべき関数群を実装DDK(ファイル名を「genprint.c」から変更)
local.h、local.cppデバッグ用の出力関数群DDK(「local.cpp」はファイル名を「local.c」から変更)
support.cppプリントプロセッサのためのサポートルーチンを含むDDK(ファイル名を「support.c」から変更)
util.cppスプールメモリ管理用関数群DDK(ファイル名を「util.c」から変更)
raw.cppRAWデータ型の印刷処理DDK(ファイル名を「raw.c」から変更)
text.cppTEXTデータ型の印刷処理DDK(ファイル名を「text.c」から変更)
parsparm.cppプリンタに対する改ページ文字の送信処理DDK(ファイル名を「parsparm.c」から変更)
emf.cppNT EMFデータ型の印刷処理。このサンプルでは、NT EMFデータ型がセパレータページ追加の対象DDKを修正(ファイル名を「emf.c」から変更)
EMFJobModifier.hEMFジョブ修正のための抽象基底クラス(CEMFJobModifier)を定義新規作成
JobSeparator.h、
JobSeparator.cpp
ジョブ修正クラスの定義と実装。ジョブ修正クラスは、CEMFJobModiferの派生クラスで、セパレータページの追加を行う新規作成

 もともと純粋なC言語で書かれていたDDKサンプルをC++に変換したため、Cの拡張子が付いていたファイルはCPPの拡張子になっています。

 新しいコードは、エクスポート関数PrintDocumentOnPrintProcessorの中で実行されます。リスト1に、この関数(「SeparatorPP.cpp」内にあります)の標準的な実装を示します。

リスト1 PrintDocumentOnPrintProcessor関数の標準的な実装(「SeparatorPP.cpp」より)
BOOL
PrintDocumentOnPrintProcessor(
    HANDLE  hPrintProcessor,
    LPWSTR  pDocumentName
)
{
   PPRINTPROCESSORDATA pData;

   /**
        Make sure the handle is valid and pick up
        the Print Processors data area.
   **/

   if (!(pData = ValidateHandle(hPrintProcessor))) {

      return FALSE;
   }

   /**
        Print the job based on its data type.
   **/

   switch (pData->uDatatype) {

   case PRINTPROCESSOR_TYPE_EMF_50_1:
   case PRINTPROCESSOR_TYPE_EMF_50_2:
   case PRINTPROCESSOR_TYPE_EMF_50_3:

      return PrintEMFJob( pData, pDocumentName );
      break;

   case PRINTPROCESSOR_TYPE_RAW:
      return PrintRawJob(pData, pDocumentName, pData->uDatatype);
      break;

   case PRINTPROCESSOR_TYPE_TEXT:
      return PrintTextJob(pData, pDocumentName);
      break;    
   } /* Case on data type */

   /** Return success **/

   return TRUE;
}

 ご覧のように、この関数は、処理の実行を印刷のデータ型に応じた適切なハンドラに委ねています。単純化のため、このサンプルではデータ型がNT EMFの印刷ジョブに限ってセパレータの追加を行っています。そのため、ここでは「emf.cpp」ファイル内のPrintEMFJob関数に注目して説明していきます。この関数は、次の2つの引数をとります。

  • PPRINTPROCESSORDATA -- ジョブデータへのポインタ
  • LPWSTR -- 文書の名前

 リスト2に、PrintEMFJob関数のコードのうち、関連する部分だけを示します(コード全体については、ソースコードを参照してください)。

リスト2 PrintEMFJob関数の関連部分のコード
BOOL
PrintEMFJob(
    PPRINTPROCESSORDATA pData,
    LPWSTR pDocumentName)

/*++
Function Description: Prints out a job with EMF data type.

Parameters:   pData           - Data structure for this job
              pDocumentName   - Name of this document

Return Value:  TRUE  if successful
               FALSE if failed - GetLastError() will return reason.
--*/

{
    . . .

   // Get spool file handle and printer device context from GDI
   try
   {

      hSpoolHandle = GdiGetSpoolFileHandle(pData->pPrinterName,
                                           pDevmode,
                                           pDocumentName);
      if (hSpoolHandle)
      {
          hPrinterDC = GdiGetDC(hSpoolHandle);
      }

   }
   catch(...)
   {
      ODS(("PrintEMFJob gave an exceptionPrinter 
%ws\nDocument %ws\nJobID %u\n",
      pData->pDevmode->dmDeviceName,
      pData->pDocument, pData->JobId));
      
      goto CleanUp;
   }
   . . .
   try
   {
      DocInfo.cbSize = sizeof(DOCINFOW);
      DocInfo.lpszDocName  = pData->pDocument;
      DocInfo.lpszOutput   = pData->pOutputFile;
      DocInfo.lpszDatatype = NULL;

      if (!GdiStartDocEMF(hSpoolHandle, &DocInfo)) goto CleanUp;
      bStartDoc = TRUE;

      ////////////////////////////////////////////////////////////
      // Adding Separator Page

      CJobSeparator JobSeparator(hPrinterDC);
      JobSeparator.ModifyPrintout();

      // End of Adding Separator Page
      ////////////////////////////////////////////////////////////

      . . .

   }
   catch (...)
   {
      . . .
   }
   . . .
   SetLastError(LastError);
    
   return bReturn;
}

 コード中の太字で記された行が、セパレータページの機能をサポートするために修正した箇所です。NT EMFデータ型の印刷ジョブを処理する最初の部分に、プリンタのコンテキスト(hPrinterDC)を取得する箇所があります。

hSpoolHandle = GdiGetSpoolFileHandle(pData->pPrinterName,
                                            pDevmode,
                                            pDocumentName);
if (hSpoolHandle)
{
   hPrinterDC = GdiGetDC(hSpoolHandle);
}

 このコードでは、セパレータページの追加を行うためにこのハンドルを使用します。PrintEMFJob関数によって文書の印刷処理が開始された直後に、CJobSeparatorのインスタンスを作成して、印刷ジョブの修正を行います。

DocInfo.cbSize = sizeof(DOCINFOW);
DocInfo.lpszDocName  = pData->pDocument;
DocInfo.lpszOutput   = pData->pOutputFile;
DocInfo.lpszDatatype = NULL;

if (!GdiStartDocEMF(hSpoolHandle, &DocInfo)) goto CleanUp;
bStartDoc = TRUE;

////////////////////////////////////////////////////////////
// Adding Separator Page

CJobSeparator JobSeparator(hPrinterDC);
JobSeparator.ModifyPrintout();

// End of Adding Separator Page
////////////////////////////////////////////////////////////

 実際のジョブ修正は、ModifyPrintout関数内で行われます。この関数はCJobSeparatorクラス(リスト3を参照)に実装されています。

リスト3 ジョブ修正の実装(CJobSeparatorクラス内)
#include ".\jobseparator.h"
#include <<TCHAR.H>>
#include <<stdio.h>>
#include <<io.h>>


#define  SEPARATOR_BITMAP_FILE  _T("C:\\SeparatorPP\\Separator.bmp")

bool CJobSeparator::ModifyPrintout()
{
   HBITMAP hbmpSeparator = NULL;
   LPBITMAPINFO pBitmapInfo = NULL;
   try
   {
      hbmpSeparator = ::LoadImage(NULL,
                        SEPARATOR_BITMAP_FILE,
                        IMAGE_BITMAP,
                        0, 0,
                        LR_LOADFROMFILE |
                        LR_CREATEDIBSECTION);
      if (NULL == hbmpSeparator)
      {
         DWORD dwErr = ::GetLastError();
         throw false;
      }

      BITMAP bm;
      ::GetObject(hbmpSeparator, sizeof(BITMAP), (LPVOID)&bm);

      pBitmapInfo = RetrieveBitmapInfo(hbmpSeparator, bm);
      if (NULL == pBitmapInfo)
      {
         // Unable to retrieve Bitmap Info
         throw false;
      }

      ::StartPage(m_hPrinterDC);

      // Stretching all the bitmap to Printer Device Context
      StretchDIBits(m_hPrinterDC,
            500, 1750,
            1000, 1000,
            0,
            0,
            bm.bmWidth,
            bm.bmHeight,
            bm.bmBits,
            pBitmapInfo,
            DIB_RGB_COLORS,
            SRCCOPY);

      ::EndPage(m_hPrinterDC);
      throw true;
   }
   catch(bool bRes)
   {
      if (NULL != hbmpSeparator)
      {
         ::DeleteObject(hbmpSeparator);
      }
      if (NULL != pBitmapInfo)
      {
         free(pBitmapInfo);
      }
      return bRes;
   }
   catch(...)
   {
      if (NULL != hbmpSeparator)
      {
         ::DeleteObject(hbmpSeparator);
      }
      return false;
   }
}

 StartPage APIは、新しいページの処理を開始します。「C:\SeparatorPP\Separator.bmp」ファイルから読み込んだビットマップ画像はこのページに表示されます。ModifyPrintoutは、このビットマップをプリンタ装置のコンテキストに合わせて伸縮させる前に、ヘルパーメソッドRetrieveBitmapInfoを使って、BITMAPINFO構造体を作成します。最後に、EndPageの呼び出しによって、セパレータページの印刷処理が終了します。

 セパレータページの追加が終わると、PrintEMFJobが、GDIのAPIを利用したEMFレコードの読み取りと、特定のプリンタ言語へのレコードの変換を行うことによって、印刷ジョブが完了します。

プリントプロセッサのインストール

 プリントサーバーまたはローカルコンピュータに自作のプリントプロセッサをインストールするには、次の手順に従います(もちろん、アプリケーション用インストーラを作成すれば、この手順を自動化できます)。

  1. コンパイルしたDLLを、次に示すプリントプロセッサのフォルダにコピーします。
  2. %SYSTEMROOT%\system32\spool\prtprocs\w32x86
    
  3. 新しいレジストリキー「Separator」を既存の「HKLM\SYSTEM\CurrentControlSet\Control\Print\Environments
  4. \Windows NT x86\Print Processors」キーの下に作成します。
  5. 新たに作成した「Separator」キーに、ドライバのREG_SZ型変数を追加し、その値としてプリントプロセッサDLLの名前を指定します(この場合は、「Driver=SeparatorPP.dll」)。
  6. サービスアプレットまたは次のコマンドを用いて、Windowsスプーラサービスを再起動します。
  7. net stop spooler
    net start spooler
    
    ご参考までに、レジストリスクリプトファイル(SeparatorPP.reg)をこの記事のダウンロードサンプルに収録しています。
  8. コントロールパネルの[プリンタとFAX]を選択し、使用するプリンタの[プロパティ]ウィンドウを開きます。
  9. [詳細設定(Advanced)]タブ(図2を参照)を選択し、[プリントプロセッサ...(Print Processor...)]ボタンをクリックします。表示されるリストから、自作のプリントプロセッサを選択します(図3を参照)。既定のデータ型はRAWのままにしておきます(この設定は、アプリケーションによってデータ型が指定されない場合にのみ関係します。大部分のNTシステムは印刷ジョブをEMF形式でスプールするため、そうなることは非常にまれです)。
  10. 図2 プリンタのプロパティ
    図2 プリンタのプロパティ
    図3 プリントプロセッサの選択
    図3 プリントプロセッサの選択
著者からの注意
 レジストリの修正には注意が必要です。場合によっては、お使いの機器が動作しなくなる恐れがあります。

自作プリントプロセッサのデバッグ

 すでに説明したように、プリントプロセッサはWindows印刷スプーラサービスによって実行されます。自作のアプリケーションをデバッグするためには、「spoolsv.exe」プロセスにデバッガをアタッチして、コードにブレークポイントを設定します。Microsoft Visual Studio 7を使用している場合、この操作は非常に簡単です(図4を参照)。

図4 デバッグ:「spoolsv.exe」プロセスにアタッチして、ブレークポイントをコードに設定
図4 デバッグ:「spoolsv.exe」プロセスにアタッチして、ブレークポイントをコードに設定

 デバッグセッションを開く場合は、PDBファイルをプリントプロセッサのフォルダにコピーするのを忘れないでください。

ハードワークからハードコピーへ

 それでは、文書を印刷して、自作のプリントプロセッサが正常に機能することを確認しましょう。「プリントプロセッサのインストール」での説明どおりにプリントプロセッサの関連付けを行ったプリンタで文書を印刷します。文書の印刷を行うたびに、「Separator.bmp」の画像が最初のページとして印刷されるはずです。

新しいアプリケーションの探索

 比較的シンプルなこの技法を使えば、特定のプリンタでの印刷処理を制御することができます。この記事のサンプルコードまたはDDKのコードを利用すれば、不足機能の追加、印刷ジョブの変更など、お使いのプリンタの機能強化が可能です。ただし、自作コードのテストで紙を無駄使いするのは避けてください。テストには、バーチャルプリンタ、またはファイルへの印刷を利用するか、あるいは最低でもリサイクル紙を使って印刷するようにしましょう。



  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

あなたにオススメ

著者プロフィール

  • Yevgeny Menaker(Yevgeny Menaker)

    執筆のほか、上級ソフトウェアエンジニア、コンサルタントとして活動。現在、PortAuthority Technologies社(以前のVidius社)に勤務し、コンテンツ分析用セキュリティ製品の開発、および電子メールや他のチャネルからの重要情報の漏洩に対する保護に携わっている。メールの宛先はjeka...

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5