GeckoエンジンにmacOSでprintToFileの機能を実装してみた話 - 2017-02-22 - ククログ

ククログ

株式会社クリアコード > ククログ > GeckoエンジンにmacOSでprintToFileの機能を実装してみた話

GeckoエンジンにmacOSでprintToFileの機能を実装してみた話

はじめに

Geckoエンジンはクロスプラットフォームを標榜して作成されています。また、できるだけそのプラットフォームの特長を生かすため、そのプラットフォーム特有のAPIを使用するように作られている箇所もあります。印刷に関連するGeckoのコードももちろんプラットフォームごとに異なるAPIを呼ぶようになっており、通常使用する限りにおいてはどのプラットフォームも一様に印刷の機能を使用することができます。

今回の記事は nsIWebBrowserPrint に紐づいているprintという印刷のためのAPIにmacOSではPDFを印刷するのに必要な情報が渡っていなかった問題を解決した、という題材について書きます。

Geckoでの印刷の流れ

Geckoでは印刷をするときにプラットフォーム固有のAPIを使用する箇所とプラットフォーム非依存のAPIの箇所があります。

プラットフォームに依存するモジュールはサービス化されており、実行時に適切なContract ID(CID)を持ったモジュールがロードされる仕組みとなっています。このCIDはJavaScript側からも見え、アドオンやXULアプリケーション上でもこのCIDを用いて適宜必要なモジュールをロードしていきます。

印刷の流れを追うと以下のようになります。

nsDocumentViewer::Print(...)
  -> nsPrintEngine::Print(...) 
    -> nsPrintEngine::DoCommonPrint(...) 
      -> printDeviceContext->InitForPrinting(aDevice, ...) // プラットフォームごとに別々のDeviceContextが作成されるのでそれを用いる
        -> aDevice->MakePrintTarget(..) 
    -> nsPrintEngine::DoCommonPrint(...) //戻ってきたら印刷プレビューか印刷ジョブを行う

macOSのCocoaのAPIでの実装はwidget/cocoa以下に配置されています。

macOSでprintToFileが動かない問題のBug

macOSではnsIPrintSettingsServiceの関数のprintToFileへtrueを渡してもPDFヘ出力されないというBugが長年に渡って未解決です。詳細はMozillaのBugzillaのprintToFile is busted on Mac | Mozilla Bugzillaを参照してください。

macOSでGeckoのレンダリング結果をPDFへ出力できなかったのはなぜか

macOSでPDFへの出力をさせるのに必要な情報が上記Bugのパッチがコミットされる前のコードで設定されているかどうかを見ます。

macOSでCocoaのAPIからPDF印刷を行うには | ククログ にあるように、NSPrintSaveJobNSPrintJobSavingURL のような値が設定されているかを探します。

ここで、macOSでPDF印刷ができないのはnsIPrintSettingsServiceのCIDを持つ https://hg.mozilla.org/mozilla-central/file/0ca553b86af3/widget/cocoa/nsPrintSettingsX.mm にこれらの値がないかどうかを調べれば原因が分かります。

探すと見事にそのようなことをしている箇所はありませんでした。 前の記事を元にGeckoに向けたパッチを書く必要があります。

GeckoのXPCOMで生成されたクラスのAPIの振る舞いをプラットフォーム固有にする

GeckoはXPCOMの技術を用いており、インターフェースはidlファイルで定義されています。 ここではnsIPrintSettingsのidlは https://dxr.mozilla.org/mozilla-central/source/widget/nsIPrintSettings.idl です。

GeckoはC++で書かれているため、nsPrintSettings クラスに定義されているメソッドで必要なものをoverrideしてやれば目的は達成できます。

そのため、SetToFileName(const char16_t *aToFileName) をoverrideします。

diff --git a/widget/cocoa/nsPrintSettingsX.h b/widget/cocoa/nsPrintSettingsX.h
--- a/widget/cocoa/nsPrintSettingsX.h
+++ b/widget/cocoa/nsPrintSettingsX.h
@@ -51,8 +51,10 @@ public:
   void SetInchesScale(float aWidthScale, float aHeightScale);
   void GetInchesScale(float *aWidthScale, float *aHeightScale);

+  NS_IMETHOD SetToFileName(const char16_t *aToFileName) override;
+
   void SetAdjustedPaperSize(double aWidth, double aHeight);
   void GetAdjustedPaperSize(double *aWidth, double *aHeight);

diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
--- a/widget/cocoa/nsPrintSettingsX.mm
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -270,3 +275,30 @@ void nsPrintSettingsX::GetAdjustedPaperS
   *aWidth = mAdjustedPaperWidth;
   *aHeight = mAdjustedPaperHeight;
 }
+
+NS_IMETHODIMP
+nsPrintSettingsX::SetToFileName(const char16_t *aToFileName)
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  NSMutableDictionary* printInfoDict = [mPrintInfo dictionary];
+  nsString filename = nsDependentString(aToFileName);
+
+  NSURL* jobSavingURL =
+      [NSURL fileURLWithPath: nsCocoaUtils::ToNSString(filename)];
+  if (jobSavingURL) {
+    [printInfoDict setObject: NSPrintSaveJob forKey: NSPrintJobDisposition];
+    [printInfoDict setObject: jobSavingURL forKey: NSPrintJobSavingURL];
+  }
+  NSPrintInfo* newPrintInfo =
+      [[NSPrintInfo alloc] initWithDictionary: printInfoDict];
+  if (NS_WARN_IF(!newPrintInfo)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  SetCocoaPrintInfo(newPrintInfo);
+  [newPrintInfo release];
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}

ここまででPDFに出力するための関数の実装です。

まだmacOS向けにはoverrideする必要がある関数が残っています:

  • 用紙の単位

    • NS_IMETHOD SetPaperSizeUnit(int16_t aPaperSizeUnit) override;
  • 拡大縮小倍率

    • NS_IMETHOD SetScaling(double aScaling) override;
  • 縦横

    • NS_IMETHOD GetOrientation(int32_t *aOrientation) override;

    • NS_IMETHOD SetOrientation(int32_t aOrientation) override;

  • 余白(GeckoではMerginに加えてUnwritaebleMerginという設定値が存在する)

    • NS_IMETHOD SetUnwriteableMarginTop(double aUnwriteableMarginTop) override;

    • NS_IMETHOD SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) override;

    • NS_IMETHOD SetUnwriteableMarginBottom(double aUnwriteableMarginBottom) override;

    • NS_IMETHOD SetUnwriteableMarginRight(double aUnwriteableMarginRight) override;

これらと、印刷用紙の単位変換を行なう処理を追加したものがこちらです。

このmozilla-centralのパッチではGeckoが印刷に用いている二つの単位系の対応も入っています。

  • インチ

    • kPaperSizeInches
  • ミリメーター

    • kPaperSizeMillimeters

の両方の単位系がGeckoでの印刷ではサポートされています。

そのため、

  • Inches -> Twips -> Pixels

  • Millimeters -> Twips -> Pixels

の両方を取り扱う必要があります。これは

diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
--- a/widget/cocoa/nsPrintSettingsX.mm
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -254,8 +254,13 @@ NS_IMETHODIMP nsPrintSettingsX::SetPaper
 NS_IMETHODIMP
 nsPrintSettingsX::GetEffectivePageSize(double *aWidth, double *aHeight)
 {
-  *aWidth  = NS_INCHES_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
-  *aHeight = NS_INCHES_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  if (kPaperSizeInches == GetCocoaUnit(mPaperSizeUnit)) {
+    *aWidth  = NS_INCHES_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
+    *aHeight = NS_INCHES_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  } else {
+    *aWidth  = NS_MILLIMETERS_TO_TWIPS(mAdjustedPaperWidth / mWidthScale);
+    *aHeight = NS_MILLIMETERS_TO_TWIPS(mAdjustedPaperHeight / mHeightScale);
+  }
   return NS_OK;
 }

diff --git a/widget/cocoa/nsDeviceContextSpecX.mm b/widget/cocoa/nsDeviceContextSpecX.mm
--- a/widget/cocoa/nsDeviceContextSpecX.mm
+++ b/widget/cocoa/nsDeviceContextSpecX.mm
@@ -49,6 +49,19 @@ NS_IMETHODIMP nsDeviceContextSpecX::Init
   if (!settings)
     return NS_ERROR_NO_INTERFACE;

+  bool toFile;
+  settings->GetPrintToFile(&toFile);
+
+  bool toPrinter = !toFile && !aIsPrintPreview;
+  if (!toPrinter) {
+    double width, height;
+    settings->GetEffectivePageSize(&width, &height);
+    width /= TWIPS_PER_POINT_FLOAT;
+    height /= TWIPS_PER_POINT_FLOAT;
+
+    settings->SetCocoaPaperSize(width, height);
+  }
+
   mPrintSession = settings->GetPMPrintSession();
   ::PMRetain(mPrintSession);
   mPageFormat = settings->GetPMPageFormat();

のコードにより取り扱うことができます。

ここではGetCocoaUnitkPaperSizeMillimeterskPaperSizeInches を返すprotectedメソッドです。また、SetCocoaPaperSizeは印刷用紙の大きさを設定するメソッドです。

まとめ

macOSでもXULアプリケーションなどから printToFile = true を設定したときにPDFへ出力するパッチについて解説しました。 このBugのレポート日時は 2011-08-01 12:35 PDT なので、このパッチで5年半越しの問題が解決されました。 mozilla-centralの該当コミットを見るとBug番号がまだ6桁であり、周辺のコミットに紐づいたBug番号は7桁なので感慨深いですね。