Node.jsでブログサムネを自動生成|sharp + canvasで画像にテキスト描画

概要

本記事では、Node.jsのsharpと@napi-rs/canvasを使ってブログサムネを自動生成する方法について解説していきます。

というのも記事を作るためにサムネイルを作成する必要があり「画像編集ツールを開く → 編集 → 出力 → アップロード」という作業を毎回やるのが超絶面倒(- -;

ここではそんな悩みを解決できるように、コマンド一発で背景画像+日本語テキストのサムネイルを描画できる仕組みを紹介していきます!

ここの方法を使えば毎回のサムネ作成作業がゼロになります。
それではやっていきましょう!

目次

実装手順

1.npmでライブラリをインストール

今回の実装で使用するライブラリをnpmでインストールします。
sharpとcanvasを使用してます。

コマンド

1
npm install sharp @napi-rs/canvas

2.使用したいフォントを取得

画像に配置する文字のフォントをダウンロードします。
自分の好きなフォントでOKです!

自分の場合は「SourceHanSansJP-Bold.otf」を使用しました。

ファイルの形式はttfやotfなら無問題です(^^b

3.JavaScriptを書く

JavaScriptで下記のようにソースコードを書きます。

image_text.js

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import sharp from "sharp";
import { createCanvas, registerFont, loadImage } from "canvas";
import path from "path";
import { fileURLToPath } from "url";

// ------------------------------------------------------
// 定数定義
// ------------------------------------------------------
const bgPath = "background.jpg";
const fontPath = "SourceHanSansJP-Bold.otf";
const outPath = "C:\\cover";
const fontSize = 128;

// 半角判定(英数字・記号)
function isHalfWidth(ch) {
return /^[\x00-\x7F]$/.test(ch);
}

// ------------------------------------------------------
// 1行分を1文字ずつ描画する関数
// ------------------------------------------------------
function drawTextLine(ctx, text, xCenter, y, fontSize) {
let x = xCenter - ctx.measureText(text).width / 2;

for (const ch of text) {
const w = ctx.measureText(ch).width;

ctx.fillText(ch, x + w / 2, y);

x += w;
}
}

// ------------------------------------------------------
// テキスト内に"\n"という文字列があった場合、改行を付与
// ------------------------------------------------------
function splitTextWithNewline(ctx, text, maxWidth) {
const rawLines = text.split("\\n"); // ← "\\n" という文字列で区切る
const result = [];

rawLines.forEach(segment => {
const wrappedLines = wrapText(ctx, segment, maxWidth);
result.push(...wrappedLines);
});

return result;
}

// ------------------------------------------------------
// テキストを画像幅に合わせて自動改行
// ------------------------------------------------------
function wrapText(ctx, text, maxWidth) {
const lines = [];
let line = "";

for (const char of text) {
const testLine = line + char;
const { width: testWidth } = ctx.measureText(testLine);

if (testWidth > maxWidth && line !== "") {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
if (line) lines.push(line);

return lines;
}

// ------------------------------------------------------
// メイン処理
// ------------------------------------------------------
async function drawTextOnImage(backgroundFile, text, ouputPath,filename, fontFile) {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const backgroundPath = path.join(__dirname, backgroundFile);

const image = sharp(backgroundPath);
const { width, height } = await image.metadata();

const fontPath = path.join(__dirname, fontFile);
registerFont(fontPath, { family: "SHSJP" });

const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");

const bgImage = await loadImage(backgroundPath);
ctx.drawImage(bgImage, 0, 0, width, height);

ctx.font = `${fontSize}px 'SHSJP'`;
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";

// --------------------------------------------------
// 自動改行処理(最大横幅は余裕を持たせて 90% に設定)
// --------------------------------------------------
const maxTextWidth = width * 0.9;
const lines = splitTextWithNewline(ctx, text, maxTextWidth);

// --------------------------------------------------
// すべての行を中央に配置するための Y 計算
// --------------------------------------------------
const lineHeight = fontSize * 1.2;
const totalHeight = lines.length * lineHeight;
const startY = height / 2 - totalHeight / 2 + lineHeight / 2;

// --------------------------------------------------
// 行ごとに描画
// --------------------------------------------------
// メイン描画部分に組み込む
lines.forEach((line, i) => {
const y = startY + i * lineHeight;
drawTextLine(ctx, line, width / 2, y, fontSize);
});

// --------------------------------------------------
// 画像に出力
// --------------------------------------------------
// const filename = filename.replace(/[\\/:*?"<>|]/g, "_");
const outputImagePath = path.join(ouputPath, filename + ".jpg");

const outBuffer = canvas.toBuffer("image/png");
await sharp(outBuffer)
.jpeg({ quality: 90 })
.toFile(outputImagePath);

console.log("出力完了:", outputImagePath);
}

// ------------------------------------------------------
// 実行
// ------------------------------------------------------
const [, , filename, text] = process.argv;

if (!filename || !text) {
console.log("使い方: node image_text.js <出力ファイル名> <テキスト>");
process.exit(1);
}

drawTextOnImage(bgPath, text, outPath,filename, fontPath);

また定数定義部分については、個人の環境によってご変更ください

  • 変更箇所
    • const bgPath = “[背景に使用する画像]”;
    • const fontPath = “[フォント(otfなど)]”;
    • const outPath = “[完成画像の出力フォルダ]”;

4.フォルダに配置

任意のフォルダに「手順.2のフォント」と「手順.3のプログラム」を配置します。

自分の場合は下記の画像のようなフォルダ構成
サムネイルの出力結果

この記事のプログラムをそのまま使用するなら、 同じフォルダにすべてのファイルを配置しましょう!

5.プログラムを実行

プログラムを実行して画像を生成してみます。

コマンド

1
node C:/tool/image_text.js "test_test" "テストですよ\nテスト123テストhogeテスト123テスト"
実行時に自分の場合は「couldn't load font」エラーが発生しました。
うまくフォントが読み込めて無いようです。。。
本当は修正したいのですが、今回の場合は問題なく画像出力できたのでここでは無視してます。

6.確認

出力された画像を確認します。
無事出力されてそうですね!

サムネイルの出力結果

動作しない場合

動作しない場合はcanvasの再インストールを試してみると動作するかもしれません

コマンド

1
2
npm uninstall @napi-rs/canvas
npm install canvas

締め

こんな感じでサムネ生成ツールを作ってみました。

結構サムネイルを作る効率が上がったのではないでしょうか?

10分程度の作業時間が1分にできるんですから、塵積で大幅な時間短縮になりますよね!

最初はpythonで実装しようとしたのですが、自分のブログでの使用言語がjsだったので、こっちで実装してみました。
結構苦労はしましたが、無事実装できてよかったです(^^

こんな感じの便利ツールは実際の業務とかでも作ったりしているので、できる範囲でどんどん記事にしてきますね!

今回は以上となります。
ツール作りは楽しいので、是非皆さんもやりましょう!
お疲れさまでした。