mottox2 blog

ウェブ技術で縦書きを含む画像を生成したい

dev

ここ最近、Web技術を利用した画像生成に興味があります。本記事では、日本語における表現の一種である縦書きに焦点を当て、Web技術を使った縦書きを含む画像生成方法についての調査をまとめました。

現状の縦書き対応と問題点

単なる縦書きはやりやすくなっている

ウェブ上の縦書き対応は着実に進んできています。CSSでwriting-modeを使えば比較的容易に実現できます。しかし、HTMLから画像を生成する分野では話が違います。
HTML(Likeなもの)を使った画像生成で代表的な手法であるSatoriやhtml2canvasではどちらともwriting-modeをサポートしていないため、縦書きを含む画像を生成できません。
別アプローチのSVG foreignObjectを使った画像出力方法では、縦書きにはある程度対応できますが、カスタムフォントの埋め込みが難しいという課題があります。画像を作るからにはある程度デザイン性の高いものを生成したいため、カスタムフォントが使えないと話にならないケースが多いと思います。
これらの結果から、CSSを活かしたような画像生成は難しいように思えます。

デザインツールにおける縦書き実現方法の調査

例えば、デザインツールのFigmaでは縦書きに対応していませんが、Canvaは対応しています。そこで、Canvaがどのように縦書きを実現しているのか調べました。

CanvaのUIを調べると、Canvas実装ではなく、HTML構造を元にしていることがわかります。また、ダウンロード処理を見ると、エクスポートのキューに入り、処理が終わるとダウンロードされていることから、サーバーで何らかの処理が行われていると推測できます。
これにより、ヘッドレスブラウザを使用している可能性が高いと考え、調べたところ、以下の記事がヒットしました。

To convert a design from a rich web representation to PNG entirely on the server side, Canva loads the design into Headless Chromium and requests a PNG export of the page.
リッチなウェブ表現をPNGに変換するために、CanvaはデザインをHeadless Chromiumでロードし、ページのPNG出力をリクエストします。

つまり、クライアントで構築したHTMLをサーバーサイドにあるヘッドレスブラウザを使って画像に変換することで縦書きを含む画像の出力をしていることがわかりました。
ヘッドレスブラウザを使えば、カスタムフォントの扱い等でオリジンなどを気にせず、ローカルリソースに置き換えることができるため、扱いが容易になります。

この方法を使えばWeb技術を使って縦書き画像をつくれそうです。ただし、自分の調査しているものはクライアント上で簡潔させたいので別の方法で考えた方が良さそうです。

クライアントサイドでのカスタムフォント対応

カスタムフォントを使用しながら、クライアントサイドで完結させる方法を模索しました。原始的ではありますが、1文字ずつレンダリングしていく方法が賢明だと思いました。簡易な実装ではありますが、以下のような方針です。

参考コード
export const renderVerticalText = (
  ctx: CanvasRenderingContext2D,
  word: string,
  sx: number = 0,
  sy: number = 0,
) => {
  const fontSize = 24;
  let height = sy;
  ctx.font = `bold ${fontSize}px vertical-font`; // 縦書き用のフォントを使う必要がある
  const words = splitWords(word); // 単語ごとに分割する
  words.forEach((word, i) => {
    ctx.save();
    ctx.translate(sx, height);
    if (word.match(/[「(【『]/)) {
      // 文字を半分だけ上にずらす
      ctx.fillText(word, 0, fontSize - 2 - fontSize / 2);
      height += (fontSize / 2);
    } else if (word.match(/[」)】、。』]/)) {
      ctx.fillText(word, 0, fontSize - 2);
      height += (fontSize / 2);
    } else if (word === " ") {
      height += (fontSize / 2);
    } else if (word.length > 1) {
      const padding = (fontSize / 5);
      // 前後が空白か確認して、空白でなければpaddingを足す
      // 本当は括弧系の場合も考慮するべき
      const paddingStart = i > 0 && words[i - 1] !== " " ? padding : 0;
      const paddingEnd = words[i + 1] !== " " ? padding : 0;
      console.log(paddingStart, paddingEnd);
      const size = ctx.measureText(word);
      height += size.width * scaleY + paddingStart;
      ctx.rotate(Math.PI / 2);
      ctx.fillText(word, 1 + paddingStart, -2);
      height += paddingEnd;
    } else {
      // 全角系の文字
      height += fontSize * scaleY;
      const { width } = ctx.measureText(word);
      ctx.fillText(word, (fontSize - width) / 2, fontSize - 2);
    }
    ctx.restore();
  });

  return { height: height - sy };
};

この方法であれば、Satoriやhtml2canvasなどの既存のツールを使わずとも、縦書き対応が可能です。ただし、この方法では制御するためのコードが複雑になりやすい上に、描写上の問題がいくつか残っている点に注意してください。
また、この処理を行う際に利用するフォントは縦書き用のフォントを使わなければいけません。CanvasのfillTextでは縦書き用のグリフを指定できないので、縦書き用のフォントを作らないとーや『』などの鉤括弧類の方向が誤ったまま表示されます。

まとめ

縦書き用の画像を動的に生成するには、まだそれなりの努力が必要そうです。もし、縦書きに関する詳細な知識をお持ちの方がいらっしゃれば、ツッコミをいただきたいです。

blog

Netlify Formsで問い合わせフォームを作ったら簡単だった

追記(2022/12/29): 問い合わせに対応する窓口をTwitterに統一したいので、フォームページは削除しました。 当ブログは静的サイトホスティングサービスのNetlifyでホスティングされ

netlify
dev

翻訳でHacktoberfestに参加しました

毎年10月に開催されるHacktoberfestに参加しました。このイベントはOSSへの貢献を行い、期間中に規定数(4つ)の貢献を行った人に特典がプレゼントされるものになっています。 自分はドキュメ

event

Figmaのイベントでプラグイン開発について話してきた

Figmaの公式コミュニティであるFriends of Figma, TokyoのFigmaお楽しみトーク Vol.2というイベントでFigmaプラグイン開発について話してきました。 今回のイベント

Copyright © 2023 @mottox2 All Rights Reserved.