RustでOG画像をどこでも生成できるライブラリを作りました。
Overview
og_image_wrirter というOG画像を生成できるRustのライブラリを作りました。
このライブラリはwasmを通してどこでも動きます。WebでもEdgeでも動きます。
wasiでも動くので、試せてないですがFastlyのlucetなどのruntimeでも動くはずです。
先日Cloudflare Workersで実際に動かす記事を書きました。
APIはCSS likeなスタイリングをサポートしており、比較的直感的にスタイリングできます。(現状、完全に仕様に準拠しているわけではないです、、)
今回は主にRustでの使い方について紹介します。
また、現状最低限の文字列操作と画像操作、単純なレイアウトしかサポートしていないので他にも欲しい機能などあればissueを投げていただけるととても嬉しいです。
使い方
背景画像に対して描画する
以下のような背景画像に対して
中央揃えで文字を表示する方法を紹介します。
実装例は以下のような感じです。
use og_image_writer::{style, writer::OGImageWriter, img::ImageInputFormat};
use std::path::Path;
fn main() -> anyhow::Result<()> {
let text = "This is Open Graphic Image Writer for Web Developer.";
let mut writer = OGImageWriter::from_data(
style::WindowStyle {
align_items: style::AlignItems::Center,
justify_content: style::JustifyContent::Center,
..style::WindowStyle::default()
},
// 背景画像を読み込む
include_bytes!("../../assets/og_template.png"),
// 背景画像の画像形式を選ぶ
ImageInputFormat::Png,
)?;
let font = Vec::from(include_bytes!("../../fonts/Mplus1-Black.ttf") as &[u8]);
writer.set_text(
text,
style::Style {
margin: style::Margin(0, 20, 0, 20),
line_height: 1.8,
font_size: 100.,
word_break: style::WordBreak::Normal,
color: style::Rgba([255, 255, 255, 255]),
text_align: style::TextAlign::Start,
..style::Style::default()
},
Some(font),
)?;
let out_dir = "../examples";
let out_filename = "output_background_image.png";
writer.generate(Path::new(&format!("{}/{}", out_dir, out_filename)))?;
Ok(())
}
まず OGImageWriter
についてですが、このstructが画像を描画するための構造体です。
OGImageWriter::from_data
はdata(画像)を背景とする画像を生成するための構造体を返します。
OGImageWriter::from_data
以外にもOGImageWriter::new
があり、こちらは直接背景色を指定することができて、それが背景となります。
WindowStyle
はwindow、つまり背景画像のスタイルを示します。windowの上に文字などを描画していきます。
HTMLで表すと以下のような状態です。
<div class="window">
<span>windowの上に文字を描画する</span>
</div>
次に文字の描画についてです。
writer.set_text(
text,
style::Style {
margin: style::Margin(0, 20, 0, 20),
line_height: 1.8,
font_size: 100.,
word_break: style::WordBreak::Normal,
color: style::Rgba([255, 255, 255, 255]),
text_align: style::TextAlign::Start,
..style::Style::default()
},
Some(font),
)?;
文字の描画にはwriter.set_text
を使います。これはOGImageWriter
のメソッドです。
HTMLで表すと以下のような状態です。
<div class="window">
<span class="text">This is Open Graphic Image Writer for Web Developer.</span>
</div>
第1引数に描画したいテキストを入力し、第2引数にStyle
構造体を入れます。また、第3引数にはfontを指定します。
wasmで動くようにする関係でfontは必ず外部から読み込む必要があります。
Style
はwindow以外の要素のスタイリングに用います。font_size
やmargin
などのいろいろなスタイルを使えます。
最後に画像の描画 + 画像ファイルの生成をします。これを行うにはwriter.generate
を使います。
writer.generate(Path::new(&format!("{}/{}", out_dir, out_filename)))?;
この関数は描画と画像生成までを行います。もし、描画だけして画像生成は違うプロセスで行いたい場合は、writer.pain
を使います。
さらに、描画された画像をvectorとして吐き出したい場合はwriter.into_vec
を使います。
writer.pain();
let data = writer.into_vec();
do_somthing(data);
画像の描画
画像を描画する方法について紹介します。
以下のような画像を生成します。
以下が実装例です。
use og_image_writer::{img::ImageInputFormat, style, writer::OGImageWriter, Error};
use std::path::Path;
fn main() -> anyhow::Result<()> {
let text = "This is Open Graphic Image Writer for Web Developer.";
let mut writer = OGImageWriter::new(style::WindowStyle {
width: 1024,
height: 512,
background_color: Some(style::Rgba([70, 40, 90, 255])),
align_items: style::AlignItems::Center,
justify_content: style::JustifyContent::Center,
..style::WindowStyle::default()
})?;
writer.set_img_with_data(
include_bytes!("../../../assets/thumbnail_circle.png"),
100,
100,
ImageInputFormat::Png,
style::Style {
margin: style::Margin(0, 20, 0, 20),
position: style::Position::Absolute,
text_align: style::TextAlign::End,
top: Some(20),
left: Some(0),
border_radius: style::BorderRadius(50, 50, 50, 50),
..style::Style::default()
},
)?;
let font = Vec::from(include_bytes!("../../../fonts/Mplus1-Black.ttf") as &[u8]);
writer.set_text(
text,
style::Style {
margin: style::Margin(0, 20, 0, 20),
line_height: 1.8,
font_size: 50.,
color: style::Rgba([255, 255, 255, 255]),
text_align: style::TextAlign::End,
max_height: Some(150),
text_overflow: style::TextOverflow::Ellipsis,
position: style::Position::Absolute,
bottom: Some(0),
right: Some(0),
..style::Style::default()
},
Some(font),
)?;
let out_dir = "../examples";
let out_filename = "output_absolute.png";
writer.generate(Path::new(&format!("{}/{}", out_dir, out_filename)))?;
Ok(())
}
大体は最初の例と同じなので省きます。
画像を描画する関数であるwriter.set_img_with_data
について説明します。
writer.set_img_with_data(
include_bytes!("../../../assets/thumbnail_circle.png"),
100,
100,
ImageInputFormat::Png,
style::Style {
margin: style::Margin(0, 20, 0, 20),
position: style::Position::Absolute,
text_align: style::TextAlign::End,
top: Some(20),
left: Some(0),
border_radius: style::BorderRadius(50, 50, 50, 50),
..style::Style::default()
},
)?;
第1引数に描画したい画像を指定します。画像を描画するメソッドは2種類あります。set_img_with_data
はdataを受け取り、set_img
はpathを受け取り自動で画像を読み込みます。
第2、第3引数にはwidthとheightを指定します。
第4引数にformatを指定し、第5引数にStyle
を入れます。
border_radius
はアンチエイリアシングに対応しています。
Container要素
writer.set_container
を使うとwriter自身をwindowに描画できます。
pub fn main() -> anyhow::Result<(), Error> {
// ...
// windowに描画するためのcontainerを定義する
let mut container = OGImageWriter::new(style::WindowStyle {
width: 500,
height: 250,
background_color: Some(style::Rgba([70, 40, 90, 255])),
align_items: style::AlignItems::Center,
justify_content: style::JustifyContent::Center,
..style::WindowStyle::default()
})?;
// ...
let mut writer = OGImageWriter::new(style::WindowStyle {
width: 1024,
height: 512,
background_color: Some(style::Rgba([255, 255, 255, 255])),
align_items: style::AlignItems::Center,
justify_content: style::JustifyContent::Center,
..style::WindowStyle::default()
})?;
// containerをwindowに描画する
writer.set_container(
&mut container,
style::Style {
margin: style::Margin(0, 0, 10, 0),
text_align: style::TextAlign::Center,
border_radius: style::BorderRadius(10, 10, 10, 10),
..style::Style::default()
},
)?;
// ...
}
完全な例はexampleを参考にしてください。
以下のようなボックスを描画できます。
HTMLで表すと以下のような感じです。
<div class="window">
<div class="container">
<span class="text">...</span>
</div>
<span class="text">Hello World</span>
</div>
いろいろなfontを使う
単語や文章によって異なるfontを使いたいことがあると思います。そのような時にはFontContext
やTextArea
が役に立ちます。
fn main() -> anyhow::Result<(), Error> {
// ...
// Set global font data
let mut fc = FontContext::new();
fc.push(Vec::from(
include_bytes!("../../../fonts/OpenSansCondensed-Light.ttf") as &[u8],
))?;
fc.push(Vec::from(
include_bytes!("../../../fonts/Roboto-Light.ttf") as &[u8]
))?;
fc.push(Vec::from(
include_bytes!("../../../fonts/Mplus1-Black.ttf") as &[u8]
))?;
let mut textarea = TextArea::new();
textarea.push_text("こんにちは。 ");
textarea.push_text("This is ");
textarea.push(
"Open Graphic Image Writer",
style::Style {
color: style::Rgba([255, 0, 255, 255]),
font_size: 100.,
..style::Style::default()
},
None,
)?;
textarea.push_text(" for ");
textarea.push(
"Web Developer(Web開発者)",
style::Style {
color: style::Rgba([255, 0, 0, 255]),
font_size: 100.,
..style::Style::default()
},
Some(Vec::from(
include_bytes!("../../../fonts/Roboto-Light.ttf") as &[u8]
)),
)?;
textarea.push_text("!!!");
writer.set_textarea(
textarea,
style::Style {
margin: style::Margin(0, 20, 0, 20),
line_height: 1.8,
font_size: 100.,
// word_break: style::WordBreak::Normal,
color: style::Rgba([255, 255, 255, 255]),
text_align: style::TextAlign::Start,
word_break: style::WordBreak::BreakAll,
..style::Style::default()
},
None,
)?;
// ...
}
完全な例はexampleから確認できます。
FontContextにはglobalで共有されるfontを追加します。ここで指定されたfontは入力された文字がset_text
やset_textarea
に指定されたfontに一致しないときに使用されます。
またwriter.paint()
が実行されたタイミングでFontContext
はリセットされます。
writer.set_textarea
はTextAreaを描画します。TextAreaは文字列の集合を表す構造体で、TextArea::new
で構造体を作った後、textarea.push
やtextarea.push_text
を使うことで一つの文字の塊ごとに文字とそのスタイルを追加できます。
HTMLで表すと以下のようなイメージです。
<p class="textarea">
<span>Hello </span>
<span>World</span>
</p>
writer.set_textarea
で指定されたスタイルはtextarea.push
またはtextarea.push_text
のスタイルに継承されます。もしtextarea.push
でスタイルが指定されていた場合にはそちらが優先されます。
fontの優先度は以下のような順番で優先度が決まります。
-
textarea.push
に指定されたStyle -
writer.set_textarea
に指定されたStyle
fontに関しても同じで、writer.set_textarea
で指定されたfontはtextarea.push
またはtextarea.push_text
のfontに継承されます。もしtextarea.push
でfontが指定されていた場合にはそちらが優先されます。
fontの優先度は以下のような順番で優先度が決まります。
-
textarea.push
に指定されたFont -
writer.set_textarea
またはwriter.set_text
に指定されたFont - FontContext
その他の例
実際の例はog_image_writer/dev/componentsに載っています。
また、成果物として生成される画像はog_image_writer/dev/snapshotsに載っています。
注意点
現状、指定された文字に対応するfontがないときはエラーを投げるようにしています。これは開発の過程で空文字で動いている時にすぐに気づけるようにするためです。そのため、exampleを参考にFontContextを使ってfallbackのfontを指定しておくと安全です。
現在はこのような挙動ですが、無数の文字を考慮するのは難しいため、対応するfontが見つからない時は豆腐や空白を表示するような修正を入れようかと考えています。
対応するかもしれないこと
- fontがない時の豆腐の描画
- 絵文字の描画
- ボーダーのサポート
- object-fit: cover; 的なやつ
- 宣言的に定義できるようにする
などなど。
これ以外で欲しい機能があったり、このリストに載っているもので早く使いたい機能があればissueなどで起票いただけると早めに実装するかもしれません。
まとめ
まだまだプロダクション等で使うとなると足りない部分が多そうですが、もし使っていただけた際にはfeedbackいただけると嬉しいです。
またPlayground兼ドキュメントみたいなものも作っています。
Discussion