はじめに
NodeJSでImageMagickのappendのようにサイズがバラバラの画像を単純に横並びで連結させたい機会があったのですが、単純すぎて中々記事が見つからなかったので備忘録的な意味合いも込めて書き残します。
やること
- sharpで画像情報取得、連結
バージョン
NodeJS: v12.14.1
sharp: v0.23.4
成果物
出来上がる画像
ソースコード
yarn init -y
yarn add sharp
ソースはこちら
https://github.com/engabesi/appendImages
まず全文を貼ります。
const sharp = require("sharp");
(async () => {
const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"];
const imageAttrs = [];
// 連結する画像の情報取得
const promises = [];
const imagePromise = path =>
new Promise(async resolve => {
const image = await sharp(path);
let width = 0,
height = 0;
await image
.metadata()
.then(meta => ([width, height] = [meta.width, meta.height]));
const buf = await image.toBuffer();
resolve({ width, height, buf });
});
imagePaths.forEach(path => promises.push(imagePromise(path)));
await Promise.all(promises).then(values => {
values.forEach(value => imageAttrs.push(value));
});
// outputする画像の設定
const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0);
const outputImgHeight = Math.max(...imageAttrs.map(v => v.height));
let totalLeft = 0;
const compositeParams = imageAttrs.map(image => {
const left = totalLeft;
totalLeft += image.width;
return {
input: image.buf,
gravity: "northwest",
left: left,
top: 0
};
});
// 連結処理
sharp({
create: {
width: outputImgWidth,
height: outputImgHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 }
}
})
.composite(compositeParams)
.toFile("output.png");
})();
これでnode index.js
で実行すると上記のように左上詰めで画像が連結されます。
解説
まずsharp
をimportします。
sharpについて詳しくは公式Documentを見てください。
https://github.com/lovell/sharp
https://sharp.pixelplumbing.com/en/stable/
const sharp = require("sharp");
画像情報取得
連結する画像パスの配列と画像情報を格納する配列を宣言します。
const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"];
const imageAttrs = [];
画像のwidth, height, bufferを取得するPromise
を作成し、Promise.all
で全画像分並列実行させます。
Promise.all
のthen
で先程宣言した画像情報を格納する配列に取得した情報をpushします
const promises = [];
const imagePromise = path =>
new Promise(async resolve => {
const image = await sharp(path);
let width = 0, height = 0;
await image
.metadata()
.then(meta => ([width, height] = [meta.width, meta.height]));
const buf = await image.toBuffer();
resolve({ width, height, buf });
});
imagePaths.forEach(path => promises.push(imagePromise(path)));
await Promise.all(promises).then(values => {
values.forEach(value => imageAttrs.push(value));
});
なぜPromise実行時(imagePromise(path)
)のthen
で処理しないのかですが、こちらのthen
は実行が完了した順に走ることになります。
今回は連結される並びを固定にしたいのでPromise.all
のthen
で処理を行っています。
出力画像設定
width
には全画像のwidth合計値、height
には全画像のheightの最大値を取得します
const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0);
const outputImgHeight = Math.max(...imageAttrs.map(v => v.height));
sharp
はcomposite
というメソッドで画像の結合処理等を行います。
そのcomposite
メソッドのparam設定を行います
let totalLeft = 0;
const compositeParams = imageAttrs.map(image => {
const left = totalLeft;
totalLeft += image.width;
return {
input: image.buf,
gravity: "northwest",
left: left,
top: 0
};
});
input
に画像のbufferを入れます。
gravity
は方角で指定します。今回左上詰めで連結させるのでnorthwest
を入れます。
left
とtop
はoffsetをpx単位で設定します。
詳しくはsharp
の公式Docを読んでください。
https://sharp.pixelplumbing.com/en/stable/api-composite/
画像出力
最後にsharp
で画像を出力します。
sharp({
create: {
width: outputImgWidth,
height: outputImgHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 }
}
})
.composite(compositeParams)
.toFile("output.png");
これでoutput.png
がrootに作成されます。
連結の余白部分が透明でなくてもよかったり、サイズを削減したい場合は、
channels
を4から3に変更してbackground
のalphaを消したり、
出力画像拡張子をjpgに変更等好きなように変更してください。
まとめ
これでreject
を使ってエラーハンドリングをしていない等、色々ゴリ押し気味ですが画像連結処理を実装できました。
一応車輪の再発明をしていないかとドキュメントを眺めましたがそれらしい処理が見当たりませんでした。
もしあれば教えていただけると助かります。