LINEのフォントとSkiaSharpを使って画像にテキストを描画してみる

モガキです、久しぶりの投稿になります。

当社のマイクロサービスで提供している機能で、テキストを描画した画像ファイルを生成する必要が出てきたので、 LINEのフォント(LINE Seed JP)とSkiaSharpを使って実装することにしました。

今日はその方法をブログの記事にしたいと思います。
(プログラミング寄りの記事は久々ですね。)

SkiaSharp とLINE Seed JP を使う理由

マイクロサービスの開発フレームワークは.NET 6を使用し、Kubernetes上で稼働させています。

C#の画像ライブラリといえば「System.Drawing」でしたが、.NET 3.0からLinux環境でのサポートが終了しています。
そこで、代わりに「SkiaSharp」を使うことにしました。
「SkiaSharp」は、Googleが開発したグラフィックスエンジン「Skia」の.NET向けの実装で、Linux環境でも動作します。

また、ファイルにフォントを埋め込む場合は、フォントのライセンスに注意しなければなりません。
個人利用の場合は無償でも、商用利用の場合は有償という場合があります。

無償で商用利用可能なフォントには、「IPAフォント」、「さわらびゴシック」など多数ありますが、
その中でも昨年10月に出たばかりの「LINE Seed JP」を使ってみることにしました。

「LINE Seed JP」は、LINE株式会社が出しているフォントです。
普段から使っているLINEアプリのフォントで見慣れている方が多いのも採用した理由の1つです。

使うもの

  • LINE Seed JPフォント
  • .NET 6
  • Skia Sharp
  • Docker

SkiaSharpで画像に文字を出力する

LINE Seedの入手とインストール

LINE SeedのWEBサイトからフォントをダウンロードします。

seed.line.me

zipを展開したら、「LINE_Seed_JP/Desktop/OTF」フォルダ内にあるフォント4個をインストールします。

App、Desktop、Webの3種類ありますが、今回はDesktopを使います。
・App:スマホアプリ用
・Desktop:デスクトップアプリ用
・Web:WEBサイト用

TTFは使わないのでインストール不要です。
OTFとTTFについては後述します。

テキストを描画してみる

早速、SkiaSharpでテキストを描画してみます。

プロジェクトの作成

VisualStudio で、.NET 6のコンソールアプリを作成しました。

nugetでSkiaSharpを追加する

nugetパッケージマネージャーで下記2つのパッケージを追加します。

  • SkiaSharp
  • SkiaSharp.NativeAssets.Linux.NoDependencies

「SkiaSharp.NativeAssets.Linux.NoDependencies」をインストールしておかないと、Dockerで動かしたときに下記の例外が発生してしまいます。

Unable to load shared library 'libSkiaSharp' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: liblibSkiaSharp: cannot open shared object file: No such file or directory

canvasの準備

画像のベースになるcanvasを作成します。
背景は青で塗りつぶしておきました。

System.Drawing と同じような感覚で使えますね。

const int imageWidth = 800;
const int imageHeight = 449;

var skImageInfo = new SKImageInfo(imageWidth, imageHeight);
using var skSurface = SKSurface.Create(skImageInfo);
using var skCanvas = skSurface.Canvas;
using var skPaint = new SKPaint()
{
    FilterQuality = SKFilterQuality.High,
    IsAntialias = true
};

// 背景を塗りつぶす
skPaint.Color = SKColors.Blue;
skCanvas.DrawRect(0, 0, imageWidth, imageHeight, skPaint);
文字を描画する

canvasに文字を描画します。

skPaint.TextSize = 48f;
skPaint.Color = SKColors.White;

skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF");
skCanvas.DrawText("てすと 漢字 AbcPqr 12345 通常", 20, 60, skPaint);

⛔⛔じつはここ、ハマリポイントです。⛔⛔
System.Drawing と SkiaSharp で指定するフォント名が異なるので気を付けてくださいね。

System.Drawing の場合

フォント名に "LINE Seed JP_OTF Regular" を指定する。

SkiaSharp の場合

フォント名に "LINE Seed JP_OTF" を指定する。

ファイルを出力する

画像ファイルをWEBPで出力してみます。

var format = SKEncodedImageFormat.Webp;
var quality = 90;

byte[] bytes = skSurface.Snapshot().Encode(format, quality).ToArray();

using var fs = new FileStream("/img/test.webp", FileMode.OpenOrCreate);
fs.Write(bytes, 0, bytes.Length);

テキストを画像に埋めることができました!🎉

背景画像を描画する

背景が真っ青だと寂しいので、背景画像を描画してみます。
青の背景の上に、透明度50%で写真を描画しました。

// 背景画像を描画する
var backgroundImage = SKBitmap.Decode("/img/background.jpg");
skPaint.Color = new SKColor(0, 0, 0, 128);
skCanvas.DrawBitmap(backgroundImage, 0, 0, skPaint);

少しいい感じになりましたね!

ちなみに、この写真は当社が入居している渋谷クロスタワーからの眺めです。
ビルの合間に見える首都高が、都会の感じを出していていいですね😁

いろいろな太さで出してみる

「LINE Seed JP」には、通常の太さに加えて、太字、極細、極太の4種類が使えます。

テキストの太さを変えて描画する場合は、このように指定します。

skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF", SKFontStyle.Bold);
skCanvas.DrawText("てすと 漢字 AbcPqr 12345 太字", 20, 120, skPaint);

skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF", SKFontStyleWeight.Thin, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
skCanvas.DrawText("てすと 漢字 AbcPqr 12345 極細", 20, 180, skPaint);

skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF", SKFontStyleWeight.ExtraBold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
skCanvas.DrawText("てすと 漢字 AbcPqr 12345 極太", 20, 240, skPaint);

こちらも System.Drawing と SkiaSharp で指定方法が異なります。

System.Drawing の場合

太さごとに個別のフォント名を指定する。

太字:"LINE Seed JP_OTF Bold"
極細:"LINE Seed JP_OTF Thin"
極太:"LINE Seed JP OTF ExtraBold" ※なぜか極太のみアンダースコア無しでもOKです。

SkiaSharp の場合

フォント名に "LINE Seed JP_OTF" を指定し、太さはメソッドのオプションで変更する。

Dockerイメージを作って実行する

VisualStudioのデバッグで成功することがわかりましたので、次はDockerイメージを作って実行してみます。

DockerはWSL2のDebianにインストールしてあるものを使いました。

プロジェクトフォルダにフォントを配置する

プロジェクトフォルダ(csprojがあるフォルダ)に「fonts」フォルダを作成し、「LINE Seed JP」フォントファイルを配置します。

配置したフォントファイルは、Dockerイメージにフォントをインストールするときに使います。

Dockerfileを作成する

Microsoft Learn のチュートリアルを参考にDockerfileを作成しました。

learn.microsoft.com

Dockerfileはプロジェクトフォルダに配置しました。

ポイントは「COPY fonts /usr/share/fonts」の部分です。
このコマンドでDockerイメージにフォントをインストールすることができます。

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /App
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/runtime:6.0
WORKDIR /App
COPY --from=build /App/out .
COPY fonts /usr/share/fonts
ENTRYPOINT ["dotnet", "SkiaLineSeed.dll"]
Dockerイメージをビルドする

下記のコマンドを流してDocerイメージをビルドします。

cd "プロジェクトフォルダのパス"
docker build -t skialineseed -f Dockerfile . 

マルチステージビルドをすると、<none> という名前の中間イメージが残ってしまうので、 気になる場合は下記のコマンドで中間イメージを削除できます。

docker image prune
実行する

下記のコマンドでdockerイメージを実行します。

プロジェクトフォルダに画像ファイルが出力されれば成功です!

cd "プロジェクトフォルダのパス"
docker run -v ${PWD}:/img --rm skialineseed:latest

TTFよりもOTFがおすすめ

「LINE Seed JP」のDesktop用フォントには、OTF版とTTF版があります。

今回OTF版を使用した理由は、DockerコンテナではTTF版が使えなかったからです。
OTF版のほうがクロスプラットフォームに対応していて互換性が高いようです。

TTF版を使った場合、Windowsでは正常にテキストを描画できましたが、
Dockerコンテナで使うと画像に何も描画されなくなってしまいました。

フォントサイズが小さいと文字の高さが不揃いになる現象との闘い

通常の太さで描画したときに、Windowsでは正常に描画できるのですが、
Dockerコンテナでは文字の高さが不揃いになってしまいました。

skPaint.TextSize = 24f;
skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF");
skCanvas.DrawText("てすと 漢字 AbcPqr 1234567890", 20, 300, skPaint);

言われないと気にならないのですが、一度気になると永遠と気になり続ける現象です😭

数字の凹凸が目立ちますが、よく見るとアルファベットや日本語も不揃いになっています。

この現象を解消するのにはかなり手間取りました。

下記のように空のレイヤーに4倍のサイズでテキストを描画した後、4分の1に縮小して元のレイヤーに貼り付けることで解消できました。

var overlayImageInfo = new SKImageInfo(imageWidth * 4, imageHeight * 4);
using var overlaySurface = SKSurface.Create(overlayImageInfo);
using var overlayCanvas = overlaySurface.Canvas;

skPaint.TextSize = 24f * 4;
skPaint.Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF");
overlayCanvas.DrawText("てすと 漢字 AbcPqr 1234567890", 20 * 4, 330 * 4, skPaint);

var resizeRect = new SKRect(0, 0, imageWidth, imageHeight);
skCanvas.DrawImage(overlaySurface.Snapshot(), resizeRect, skPaint);

2倍では解消せず、4倍にしたら解消したところも厄介なところでした。
これはSkiaSharpの仕様によるものなのかもしれません。
小さく描画すると小数点の端数が丸められてしまうのではないかと考えています。

上が等倍で描画したもので、下が4倍で描画した後に4分の1に縮小したものです。
揃い具合が一目瞭然ですね!😄

SkiaSharpでインストールされているフォント一覧を取得する

作り始めはテキストが描画されず、本当にフォントがインストールされているのか何度も確認しました。

SkiaSharp でインストールされているフォント一覧は下記のように書くことで取得できます。

var fontFamilies = SKFontManager.CreateDefault().GetFontFamilies().OrderBy(x => x);
foreach (var fontFamily in fontFamilies)
{
    Console.WriteLine(fontFamily);
}

最後に

じつは私が一人で作ったかのように書いてありますが、チームメンバー4人で「あーでもない、こーでもない」と言いながら完成までこぎ着けました。

SkiaSharpやLINE Seed JPは、まだ情報が少なく、エラーの原因調査にかなり手間取りました。

同じことで困っているエンジニアの皆様の役に立つ情報になると幸いです。

ecbeingでは今まで試したことがない新しいことを一緒に試せる仲間を募集しています!

careers.ecbeing.tech