Deno で触る SVG

Deno で vercel/satori を動かせるのがざっくり確認していました。

これまで、Deno 周りで SVG を扱う手段についていくつか試みてきた中で、だいたいのユースケース(+α)に使えるものがだいたい固まったのでざっと記しておきたい。

参考

Deno で SVG を扱う

JSX(HTML) => SVG (vercel/satori)

リポジトリ名でわかる通り、Vercel が提供しているライブラリ。
これが Deno でも動作する。

シンプルに四角形を出す

try_satori_01.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "https://esm.sh/react";
import satori from "https://esm.sh/satori";

const svg = await satori(
<div
style={{
color: "black",
backgroundColor: "black",
width: 100,
height: 100,
}}
></div>,
{
width: 100,
height: 100,
fonts: [],
}
);

Deno.writeTextFileSync("dist.svg", svg);

これを用意し、次のコマンドで実行する

1
$ deno run --allow-net --allow-env --allow-write try_satori.tsx

出力結果は次の通り。

ただの四角を出力できる。

箱 in 箱

try_satori_02.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from "https://esm.sh/react";
import satori from "https://esm.sh/satori";

const svg = await satori(
<div
style={{
backgroundColor: "black",
width: 100,
height: 100,
display: "flex",
}}
>
<div
style={{
backgroundColor: "red",
width: 80,
height: 80,
margin: "auto",
}}
></div>
</div>,
{
width: 100,
height: 100,
fonts: [],
}
);

Deno.writeTextFileSync("dist.svg", svg);

これで作られた SVG は、次の様になる。

文字入れ

ここからが大事。
SVG に文字を入れることができる。

次の様にして使ってみた。

try_satori_03.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from "https://esm.sh/react";
import satori from "https://esm.sh/satori";

const fontBufferArray = await fetch(
new URL("./NotoSansJP-Black.otf", import.meta.url).toString()
).then((res) => res.arrayBuffer());

const svg = await satori(
<div
style={{
backgroundColor: "black",
width: 100,
height: 100,
display: "flex",
}}
>
<div
style={{
color: "gray",
backgroundColor: "red",
width: 80,
height: 80,
margin: "auto",
}}
>
TEXT
</div>
</div>,
{
width: 100,
height: 100,
fonts: [
{
name: "Noto",
data: fontBufferArray,
weight: 100,
style: "normal",
},
],
}
);

Deno.writeTextFileSync("dist.svg", svg);

実行結果は次の通り。

文字が埋め込まれたものが生成できる。
文字は、Path で表現されている。

画像を埋め込む

文字も入れられれば、画像も埋め込める。

try_satori_04.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React from "https://esm.sh/react";
import satori from "https://esm.sh/satori";

const fontBufferArray = await fetch(
new URL("./NotoSansJP-Black.otf", import.meta.url).toString()
).then((res) => res.arrayBuffer());

const svg = await satori(
<div
style={{
backgroundColor: "black",
width: 100,
height: 100,
display: "flex",
}}
>
<div
style={{
color: "gray",
backgroundColor: "red",
width: 80,
height: 80,
margin: "auto",
display: "flex",
flexFlow: "column",
}}
>
<div
style={{
display: "flex",
}}
>
TEXT
</div>
<div
style={{
display: "flex",
}}
>
<img
style={{
border: "3px solid #555",
borderRadius: "10px",
width: "50px",
height: "50px",
}}
src={new URL("./img.jpg", import.meta.url).toString()}
/>
</div>
</div>
</div>,
{
width: 100,
height: 100,
fonts: [
{
name: "Noto",
data: fontBufferArray,
weight: 100,
style: "normal",
},
],
}
);

Deno.writeTextFileSync("dist.svg", svg);

文字列に加えて画像を埋め込んだ。
画像は、data url で埋め込みされる

README を見ると、対応している CSS も多いので大概のレイアウトは作れるだろう。

SVG => png (svg2png-wasm)

SVG から png を作ることができるライブラリも公開されている。

ssssota/svg2png-wasm

次の様に使える。

try_svg2png_1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import * as svg2png from "https://esm.sh/svg2png-wasm";

await svg2png.initialize(
await fetch("https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm")
);

const fontBufferArray = new Uint8Array(
await (
await fetch(new URL("./NotoSansJP-Black.otf", import.meta.url).toString())
).arrayBuffer()
);

const convert_options: svg2png.ConverterOptions = {
fonts: [fontBufferArray],
defaultFontFamily: {
sansSerifFamily: "Noto Sans JP",
},
};

const inputSvg = `
<svg viewBox="0 0 600 315" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="600" height="315" style="fill: rgb(245, 245, 245);">
</rect>
<text x="50%" y="60%" text-anchor="middle" font-family="NotoSansJP" font-size="50">
TEXT
</text>
</svg>
`;

const png = await svg2png.svg2png(inputSvg, convert_options);

Deno.writeFileSync("dist.png", png);

動かすと、次の png が生成できる。

こちらは、CSS の知識というよりも純粋に SVG の知識を要求されるもの。
座標だったりをバッチリ位置決めするならこちらの楽な可能性があるが、慣れの問題と言われてもあまり異論は無い。

画像を埋め込む

画像を埋め込んでみるが、サイズ調整などはここではフォーカス外なので、埋め込むことにだけ注力する。
埋め込みは、先の satori を参考に data url を作って埋め込みを試みる。

try_svg2png_2.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import * as svg2png from "https://esm.sh/svg2png-wasm";
import { encode } from "https://deno.land/std@0.178.0/encoding/base64.ts";

await svg2png.initialize(
await fetch("https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm")
);

const fontBufferArray = new Uint8Array(
await (
await fetch(new URL("./NotoSansJP-Black.otf", import.meta.url).toString())
).arrayBuffer()
);

const convert_options: svg2png.ConverterOptions = {
fonts: [fontBufferArray],
defaultFontFamily: {
sansSerifFamily: "Noto Sans JP",
},
};

const image = Deno.readFileSync("./img.jpg");
const imageStr = encode(image);

const inputSvg = `
<svg viewBox="0 0 600 315" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="600" height="315" style="fill: rgb(245, 245, 245);">
</rect>
<text x="50%" y="60%" text-anchor="middle" font-family="NotoSansJP" font-size="50">
TEXT
</text>
<image x="0" y="0" width="100" height="100" href="data:image/jpeg;base64,${imageStr}" />
</svg>
`;

const png = await svg2png.svg2png(inputSvg, convert_options);

Deno.writeFileSync("dist.png", png);

動かすと、次の png が生成できる。

画像を埋め込んだ SVG を png 画像に変換することができた。

PSVG

PSVG は、SVG に関数と制御フローを導入した拡張。そしてそのライブラリとツール。

ライブラリでも、ツールでも動作した。
esm.sh 経由での導入も試みたが、上手く動かない。 npm から直接ロードだと上手く動作した。
Deno で、Node.js 互換性が向上しているのは事実であるが、esm.sh で動かなくて npm の直接ロードでは上手くいくケースは初めて遭遇した。

ツール

次の様に直接呼び出して使用できる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ cat src.psvg
<psvg width="500" height="500">
<def-fall_bound initial_y="0" r="1" step="100">
<var tmp_pos_y="{ initial_y }" />
<var points_y="{ CAT(initial_y, ';') }" />
<var g="9.8" />
<var sp="0" />
<var e="0.7" />

<for i="0" true="{ i < step }" step="1">
<asgn sp="{ sp + g }" />
<if true="{ tmp_pos_y + r > HEIGHT && sp > 0 }">
<asgn sp="{ (sp * e) * (-1) }" />
</if>
<if>
<cond true="{ tmp_pos_y + r > HEIGHT }">
<asgn tmp_pos_y="{ HEIGHT - r }" />
</cond>
<cond>
<asgn tmp_pos_y="{ tmp_pos_y + sp }" />
</cond>
</if>
<asgn points_y="{ CAT(points_y, tmp_pos_y, ';') }" />
</for>

<return value="{points_y}" />
</def-fall_bound>

<rect x="0" y="0" width="{ WIDTH }" height="{ HEIGHT }" fill="#000" />
<circle cx="{ WIDTH/2 }" cy="0" r="10" stroke="rgb(200,0,0)" fill="rgb(255,0,0)">
<animate attributeName="cy" values="{ fall_bound(10, 10, 200) }" dur="20s" repeatCount="indefinite" />
</circle>
</psvg>

$ deno run --allow-read --allow-write npm:@lingdong/psvg .\src.psvg > dist.svg

$ cat .\dist.svg
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" ><...

ライブラリ

PSVG はライブラリ通しても使用できた。
もし deno deploy で使いたいならこちらから使うことになるだろう

load_psvg.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { compilePSVG } from "npm:@lingdong/psvg";

console.log(
compilePSVG(
`
<psvg width="500" height="500">
<def-fall_bound initial_y="0" r="1" step="100">
<var tmp_pos_y="{ initial_y }" />
<var points_y="{ CAT(initial_y, ';') }" />
<var g="9.8" />
<var sp="0" />
<var e="0.7" />
<for i="0" true="{ i < step }" step="1">
<asgn sp="{ sp + g }" />
<if true="{ tmp_pos_y + r > HEIGHT && sp > 0 }">
<asgn sp="{ (sp * e) * (-1) }" />
</if>
<if>
<cond true="{ tmp_pos_y + r > HEIGHT }">
<asgn tmp_pos_y="{ HEIGHT - r }" />
</cond>
<cond>
<asgn tmp_pos_y="{ tmp_pos_y + sp }" />
</cond>
</if>
<asgn points_y="{ CAT(points_y, tmp_pos_y, ';') }" />
</for>
<return value="{points_y}" />
</def-fall_bound>
<rect x="0" y="0" width="{ WIDTH }" height="{ HEIGHT }" fill="#000" />
<circle cx="{ WIDTH/2 }" cy="0" r="10" stroke="rgb(200,0,0)" fill="rgb(255,0,0)">
<animate attributeName="cy" values="{ fall_bound(10, 10, 200) }" dur="20s" repeatCount="indefinite" />
</circle>
</psvg>
`
)
);

実行すると次の様になり、ツールとして実行した時同様の結果が得られる。

1
2
$ deno run load_psvg.ts
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" ><...

できたものはこちら。

入力にしたがって文字列が挿入され、且つ動く SVG なんてものも作れるので、サニタイズさえちゃんとできるなら面白そうなものが作れるはず。

難点があるとしたら、だいたい 3 年更新されていないこと。


Deno で動くSVG 周りのツールをざっと記載した。

この辺りのものを多用したものを作りたいものの、特段のネタもないのでしばらく引き出しに押し込んでおくことになりそうではある。

では。