読者です 読者をやめる 読者になる 読者になる

酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

ASP.NET MVC 5で任意のフォントをアプリに埋め込んで文字を描画された画像を出力する

本記事では、ASP.NET MVC 5で任意のフォントをEmbedded Resource(埋め込まれたリソース)としてアプリに組み込み、そのフォントを使って文字を描画し動的に画像を生成する方法を紹介します。

f:id:ch3cooh393:20140905014214p:plain

まゆるさんジェネレーターではASP.NET MVC上でゴリゴリ画像を弄ることをやっています。まるゆさんジェネレーターは100PV/日くらいの極小規模のWebサービスということもあってWebアプリ内で画像を弄っているのですがもう少し規模が大きくなった場合のベストプラクティスはどういう方法なんでしょうかね……

過去に「ASP.NET MVC 5でサーバーサイドで動的に生成した画像を返す - 酢ろぐ!」を紹介しました。さらにその応用例として「ASP.NET MVC 5で動的に生成したグラデーションを描画した画像を返す - 酢ろぐ!」を紹介しました。

ASP.NET MVCプロジェクトを作成する

とりあえず、ASP.NET MVC 5プロジェクトを作成します。ここではプロジェクト名を「WebApplication1」という名前を付けました。

f:id:ch3cooh393:20140904123325p:plain

埋め込まれたリソースへは、{プロジェクト名}.{フォルダ名}.{リソース名}のような書式でアクセスします。なのでプロジェクト名がWebApplication1と異なる場合は適切なリソース名に変えてください。

任意のフォントをプロジェクトに追加する

任意のフォントをプロジェクトに追加しましょう。WebApplication1プロジェクトのApp_Dataフォルダにフォントを追加することにします。ソリューションエクスプローラーのApp_Dataフォルダを右クリックして、コンテキストメニューから[追加]、[既存の項目]を選択します。

f:id:ch3cooh393:20140904123335p:plain

ここでは鉄瓶ゴシックを使います。なかなかフリーで使用できるフォントで極太のものがない中、このフォントは極太でゴツゴツとしています。追加するフォントファイルは07鉄瓶ゴシック.otfという名前のままにしました。

f:id:ch3cooh393:20140904123345p:plain

フォントファイルを追加すると、おそらくビルドアクションはコンテンツになっているかと思います。ビルドアクションを埋め込まれたリソースに変更します。

f:id:ch3cooh393:20140904123354p:plain

これでプロジェクトへフォントの追加が完了しました。

Views/Home/Index.cshtmlで画像を表示できるようにする

ウィザードによって生成されたプロジェクトには色々と不要なものが混じっています。Views/Home/Index.cshtmlの中身を整理しましょう。極力要素を無くしてimgタグのみのシンプルな構成にしてみました。

@{
    ViewBag.Title = "Home Page";
}

<img src="~/home/image" class="img-responsive" />

http://localhost:5033/などにアクセスされると、HomeコントローラーのIndexアクションが上記のhtmlを返します。

imgタグのsrcが~/home/imageとなっているため、、HomeコントローラーのImageアクションでのレスポンスで返した画像が表示します。

HomeコントローラーにIndexアクションとImageアクションを実装する。

こちらにも色々と不要なものが混じっています。事前に説明しやすいようにHomeController.csIndexアクション以外削除してしまいます。

using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
}

OSにインストールしていないフォントを使用するという処理でいうと、確かWinFormsとも同じ処理なので特にASP.NET MVCだからと気を付けるところは無いと思います。

OSにインストールされていないフォントを使用するには、PrivateFontCollectionクラスを使います。このPrivateFontCollectionクラスは、OSにインストールされているフォントに依存せずにクライアントアプリ側で持っているフォントを使用する機能が提供されています。

PrivateFontCollectionにフォントを追加するには、ファイルパスを指定するか、バッファを渡すかのどちらかですので、ファイルパスを指定できないAzure Webサイトで動かすためにバッファを渡す方法を選択しました。

リソースからフォントファイルをbyte型配列のfontBufferに読み出します。fontBufferのポインタとバッファの長さをPrivateFontCollectionクラスのインスタンスに渡してフォントの登録をおこないます。ポインタを使うのでunsafeを利用することになります*1

// フォントを読み込む
byte[] fontBuffer = null;
var asm = System.Reflection.Assembly.GetExecutingAssembly();
using (var strm = asm.GetManifestResourceStream(
    "WebApplication1.App_Data.07鉄瓶ゴシック.otf"))
{
    fontBuffer = new byte[strm.Length];
    strm.Read(fontBuffer, 0, fontBuffer.Length);
}

// フォントデータの読み込みと登録
var pfc = new System.Drawing.Text.PrivateFontCollection();
unsafe
{
    fixed (byte* p = fontBuffer)
    {
        pfc.AddMemoryFont((IntPtr)p, fontBuffer.Length);
    }
}

後はいつも通りFontを使ってテキストを描画します。Fontインスタンスを取得するのに決め打ちでpfc.Families[0]としていますが、本当はキチンとチェックした方が良いでしょう。

// 文字列を描画する
using (var font = new System.Drawing.Font(pfc.Families[0], 110, FontStyle.Bold))
using (var fontBrush = new SolidBrush(Color.Red))
{
    g.DrawString("繊維喪失", font, fontBrush, new PointF(0, 130));
}

unsafeコードを使うために設定を変更する

ビルドが通らないので「アンセーフ コードの許可」にチェックを入れます。

ソリューションエクスプローラーのプロジェクト名の部分を右クリックし、コンテキストメニューの[プロパティ]を選択します。

f:id:ch3cooh393:20140904123405p:plain

[ビルド]タブの全般グループに[アンセーフ コードの許可]のチェックボックスがあるので有効します。

f:id:ch3cooh393:20140904123415p:plain

これでビルドエラーになっていた部分が通るようになります。

Imageアクションの実装が完了!

HomeコントローラーのImageアクションの全体像は下記のコードのような感じです。Graphicsクラスを使ってガリガリ描画しているところはWinFormsのそれと大差ないでしょう。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Image()
        {
            // フォントを読み込む
            byte[] fontBuffer = null;
            var asm = System.Reflection.Assembly.GetExecutingAssembly();
            using (var strm = asm.GetManifestResourceStream(
                "WebApplication1.App_Data.07鉄瓶ゴシック.otf"))
            {
                fontBuffer = new byte[strm.Length];
                strm.Read(fontBuffer, 0, fontBuffer.Length);
            }

            // フォントデータの読み込みと登録
            var pfc = new System.Drawing.Text.PrivateFontCollection();
            unsafe
            {
                fixed (byte* p = fontBuffer)
                {
                    pfc.AddMemoryFont((IntPtr)p, fontBuffer.Length);
                }
            }

            // 画像を生成する
            var ms = new MemoryStream();
            using (var image = new Bitmap(640, 480))
            {
                using (var g = Graphics.FromImage(image))
                {
                    // 画像の背景色を描画する
                    using (var fillBrush = new SolidBrush(Color.White))
                    {
                        g.FillRectangle(fillBrush, 0, 0, 640, 480);
                    }

                    // 画像の枠色を描画する
                    using (var borderBrush = new SolidBrush(Color.Red))
                    using (var borderPen = new Pen(borderBrush, 2))
                    {
                        g.DrawRectangle(borderPen, 0, 0, 640, 480);
                    }

                    // 文字列を描画する
                    using (var font = new System.Drawing.Font(pfc.Families[0], 110, FontStyle.Bold))
                    using (var fontBrush = new SolidBrush(Color.Red))
                    {
                        g.DrawString("繊維喪失", font, fontBrush, new PointF(0, 130));
                    }
                }

                // 描画したビットマップをPNGとしてストリームへ保存する
                image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
                ms.Position = 0;
            }

            pfc.Dispose();

            // サーバーサイドで実装した画像を返す
            return new FileStreamResult(ms, "image/png");
        }
    }
}

完成したのでデバッグ実行してみましょう。鉄瓶フォントの強調されながらも独特な優しさが分かるかと思います。

f:id:ch3cooh393:20140905014204p:plain

ただ、ちょっと文字の周りがギザギザになっているのが気になりますね……。

(おまけ)テキストのレンダリング

文字の周りがジャギっているのはアンチエイリアス処理が有効になっていないからです。境界(ふち)部分をスムーズにするためにはGraphicsのSmoothingModeプロパティTextRenderingHintプロパティを別途指定する必要があります。

// 画像を生成する
var ms = new MemoryStream();
using (var image = new Bitmap(640, 480))
{
    using (var g = Graphics.FromImage(image))
    {
        // 塗りつぶし領域の境界線をスムーズ(アンチエイリアス処理)にする
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
        // テキストレンダリング時にアンチエイリアス処理を有効にする
        g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

        // (省略)

アンチエイリアスを有効にすると下記のように境界部分が綺麗になります。

f:id:ch3cooh393:20140905014149p:plain

ここまで書いて気付いたけど、維喪失じゃなくて維喪失だった。さらにおまけ。

f:id:ch3cooh393:20140905021402p:plain

やはりキルラキルネタは、フォントがラグランパンチじゃないと違和感が……

*1:この辺りはWin32API的な縛りなのかな?