normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

Windows Azure & ASP.NET MVC4 & itextsharp で日本語PDFファイルの動的生成

ふと思い立ち、Windows Azure 上で日本語 PDF ファイルを生成したい衝動に駆られたのでフィージビリティを取得した。結論から言うと「任意の日本語フォントを利用したPDFファイルは Windows Azure 上で生成可能」だ。
後述する ActiveReports の様に独自にフォントを管理する機能をもたなくとも、日本語フォントを Windows Azure 上にインストールして利用することが可能なことが分かった。しかし、ベンダーサポートやライセンスの問題については言及していないので、その点には留意頂きたい。

前提知識

Windows Azure のホスティッドサービスで提供される仮想マシンは英語版OSであり、日本語環境にローカライズされていない点を認識頂く必要がある。このため、日本語フォントはインストールされておらず、タイムゾーンUTC 時刻となっている。タイムゾーンは DateTime.UtcNow に対し、日本の時差である時間だけ差分をとれば対応可能だが、日本語フォントはこの様に単純な対応ではうまくいかない。
Windows Azure 上に日本語フォントが不足している環境で日本語PDFファイルを作成するには、以下のうちどちらかの対応を実施するしかない。

  • ActiveReports の様に、独自に日本語フォントを管理する機能を持つソフトウェアを利用する
  • Windows Azure 環境に日本語フォントをインストールする

Azure への日本語フォントのインストール方法に関しては、 @harutama 氏がブログで紹介しているため、今回はこちら あれとアレは混ぜるな危険 Azureのインスタンスにフォントを入れたいんですが? を利用する。

今回利用するPDF生成ライブラリ

今回は itextpdf を日本語PDFファイルの生成ライブラリとして利用する。日本語での紹介記事も存在するので、必要な場合は別途 codezine の記事を参照して頂きたい。

サンプルアプリケーションのセットアップ

Windows Azure プロジェクトに対し、ASP.NET MVC4 プロジェクトを Web ロールとして追加する。Webロールとして追加した ASP.NET MVC4 プロジェクトに対し、以下を実施する。

  • 必要な日本語ファイルの配置

特に設定を変えていない日本語 Windows 環境の場合、C:\Windows\Fonts にフォント・ファイルが配置されている。今回は meiryo.ttc をASP.NET MVC4 プロジェクト内に配置する。

  • itextpdf の DLL を配置&参照の追加

itextpdf の公式サイト から itextsharp.dll を取得し、ASP.NET MVC4 プロジェクト内に配置する。プロジェクト内に配置した itextsharp.dll に対し、[参照設定]から当該DLLに対して参照を追加する。

  • font インストール用のスクリプトを作成&配置&[出力ディレクトリにコピー]を[新しい場合はコピーする]に変更

以下はほぼ あれとアレは混ぜるな危険 Azureのインスタンスにフォントを入れたいんですが? で記載されている内容と同様であるため、必要であれば @harutama 氏のブログを参照して頂きたい。

  • font.bat
cscript //nologo font.vbs
  • font.vbs
'Windowsのフォントフォルダをとってくる
Const FONTS = &H14&
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(FONTS)

'アップロードしたパッケージのルートを取得
Set objWshShell = WScript.CreateObject("WScript.Shell")
root = objWshShell.ExpandEnvironmentStrings("%RoleRoot%")

'パッケージ内にあるフォントをWindowsのフォントフォルダにコピー
objFolder.CopyHere root & "\approot\Fonts\meiryo.ttc"

上記の*.dll, *.vbs, *.bat ファイルを ASP.NET MVC4 プロジェクト側に配置後、プロパティウィンドウから[出力ディレクトリにコピー]を[新しい場合はコピーする]に変更する。

  • startup task で font.bat を動作する処理を記載

ServiceDefinition.csdef にて、font.bat を管理者権限で実行する処理を記載する。

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="WAzureProject1" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="Mvc4PDFApp" vmsize="Small">
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Endpoint1" endpointName="Endpoint1" />
        </Bindings>
      </Site>
    </Sites>
    <Startup>
      <Task commandLine="font.bat" executionContext="elevated" taskType="background" />
    </Startup>
    <Endpoints>
      <InputEndpoint name="Endpoint1" protocol="http" port="80" />
    </Endpoints>
    <Imports>
      <Import moduleName="Diagnostics" />
      <Import moduleName="RemoteAccess" />
      <Import moduleName="RemoteForwarder" />
    </Imports>
  </WebRole>
</ServiceDefinition>

また、最終的なプロジェクトの構成は以下になるので、別途参考にして頂きたい。

サンプルアプリケーションに対するコーディング

ここからはシンプルなコーディングになるため、特に解説が不要だと思われるため、そのままソースコードを記載する。

  • HomeController

こちらはそのままで、特に変更点は無し。

using System.Web.Mvc;

namespace Mvc4PDFApp.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";

            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your quintessential app description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your quintessential contact page.";

            return View();
        }
    }
}
  • PdfController.cs

PDF生成用のコントローラを作成。

using System;
using System.Web.Mvc;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.IO;

namespace Mvc4PDFApp.Controllers
{
    public class PdfController : Controller
    {
        //
        // GET: /Pdf/

        public ActionResult EnglishText()
        {
            //TEMPディレクトリに対するPDFファイルのパスを作成
            //カレントディレクトリでは、Azure上で権限が無い
            //TODO: 各ユーザのリクエストごとにファイルが生成できるようにする必要がある
            var pdffilePath = Path.Combine(Path.GetTempPath(), string.Format("01_Hello_{0}.pdf", Session.SessionID));

            //PDFファイルを作成
            {
                Document doc = new Document();
                PdfWriter.GetInstance(doc, new FileStream(pdffilePath, FileMode.Create));
                doc.Open();
                doc.Add(new Paragraph("Hello iTextSharp"));
                doc.Close();
            }

            //通常は 1024*N バイト単位に分けてファイル読み込みを行うが、
            //今回はPDFをブラウザに返すために一括読み込み
            var fs = new FileStream(pdffilePath, FileMode.Open);
            var bytes = new byte[fs.Length];
            fs.Read(bytes, 0, (int)fs.Length);

            return File(bytes, "application/pdf");
        }

        public ActionResult JapaneseText()
        {
            //TEMPディレクトリに対するPDFファイルのパスを作成
            //カレントディレクトリでは、Azure上で権限が無い
            //TODO: 各ユーザのリクエストごとにファイルが生成できるようにする必要がある
            var pdffilePath = Path.Combine(Path.GetTempPath(), string.Format("02_Hello_{0}.pdf", Session.SessionID));

            //PDFファイルを作成
            {
                Document doc = new Document();
                PdfWriter.GetInstance(doc, new FileStream(pdffilePath, FileMode.Create));
                doc.Open();

                Font fnt1 = new Font(BaseFont.CreateFont(
                    //Windows Azure は Windowsフォルダが D: 以下のため、当該処理が必要
                    Path.Combine(Environment.GetEnvironmentVariable("windir"), @"fonts\meiryo.ttc,0")
                    , BaseFont.IDENTITY_H, true), 40);

                doc.Add(new Paragraph("メイリオフォントを利用した日本語描画", fnt1));
                doc.Close();
            }

            //通常は 1024*N バイト単位に分けてファイル読み込みを行うが、
            //今回はPDFをブラウザに返すために一括読み込み
            var fs = new FileStream(pdffilePath, FileMode.Open);
            var bytes = new byte[fs.Length];
            fs.Read(bytes, 0, (int)fs.Length);

            return File(bytes, "application/pdf");
        }
    }
}
  • Index.cshtml

PdfControllerで生成したファイルを表示するため、iframeを利用して表示している。

@{
    ViewBag.Title = "Home Page";
}
@section featured {
    <section class="featured">
        <div class="content-wrapper">
            <hgroup class="title">
                <h1>@ViewBag.Title.</h1>
                <h2>@ViewBag.Message</h2>
            </hgroup>
            <p>
                To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">
                    http://asp.net/mvc</a>. The page features <span class="highlight">videos, tutorials,
                        and samples</span> to help you get the most from ASP.NET MVC. If you have
                any questions about ASP.NET MVC visit <a href="http://forums.asp.net/1146.aspx/1?MVC"
                    title="ASP.NET MVC Forum">our forums</a>.
            </p>
        </div>
    </section>
}
<h3>
    英語だけのPDFファイル</h3>
<iframe src='@Url.Content("~/Pdf/EnglishText")' style="width:600px;">代替テキスト </iframe>
<div style="height: 10px;">
</div>
<h3>
    日本語ありのPDFファイル</h3>
<iframe src='@Url.Content("~/Pdf/JapaneseText")'style="width:600px;">代替テキスト </iframe>

こちらを Windows Azure 上にデプロイして実行すると以下の様になる。是非お試し頂きたい。

ASP.NET MVC4 beta だけの問題…?

何故か ASP.NET MVC4 beta を Azure 上にデプロイしたところ、cshtmlファイルでの文字化けが発生した。私が実施した対応は以下となる。

  • cshtml ファイルが sjis エンコーディングだったものを utf-8 にエンコーディングを変更した
  • Web.Config に以下を追記した
  <system.web>
    <globalization
     fileEncoding="utf-8"
     requestEncoding="utf-8"
     responseEncoding="utf-8"
     culture="ja"
     uiCulture="ja-JP"
        />

また、Web.Config 記載時に参照した MSDN ページは以下となる。参考のために追記しておく。

参考リンク