画像からpictureタグを自動生成する
はじめに
昨今のWeb制作では画像ファイルを使用する際に
jpg, png 画像のほか webp画像を使用するケースが一般になっています。
通常、画像を出力する際は下記のようなコードを書くのことが多いのではないでしょうか。
<picture>
<source srcset="sample.webp" type="image/webp" width="200" height="200" />
<img src="/sample.jpg" alt="" width="200" height="200" />
</picture>
ただ、上記コードを毎回書くのが結構めんどくさかったりします。
(widthとheightまで 律儀に書くところとか特に)
なので、今回コマンドを実行するだけで上のコードを自動で生成するスクリプトを作成しました。
下記コマンドで別ファイルにてスニペットが生成されます。
yarn build `html` または `pug` // “build”: “node ./scripts/picture.js”
ちなみにコマンド実行後は下記のようなスニペットを別ファイルとして生成します。
<picture>
<source srcset="/assets/img/sample_01.webp" type="image/webp" width="600" height="600" />
<img src="/assets/img/sample_01.jpg" alt="" width="600" height="600" />
</picture>
<picture>
<source srcset="/assets/img/sample_02.webp" type="image/webp" width="600" height="600" />
<img src="/assets/img/sample_02.jpg" alt="" width="600" height="600" />
</picture>
...
picture
source(srcset="/assets/img/sample_01.webp" type="image/webp" width="600" height="600")
img(src="/assets/img/hoge/sample_01.jpg" alt="" width="600" height="600")
picture
source(srcset="/assets/img/sample_02.webp" type="image/webp" width="600" height="600")
img(src="/assets/img/hoge/sample_02.jpg" alt="" width="600" height="600")
...
▶︎ 生成されたコードは適宜、使用するプロジェクトでコピペして使用してください。
流れ
仕組みとしてはざっくり下記のような流れになっております。
- jpg, png など拡張子をもつ画像を見にいく。
- 画像が保有するパス情報、サイズ情報を取得。
- 3の情報よりスニペットを生成。配列で格納。
- 外部ファイルを生成後、3の情報をファイルの中に展開する。
全体のコード
下記全体のスクリプトになります。(汚くてスミマセン)
const fs = require('fs')
const glob = require("glob");
const sizeOf = require("image-size");
const ALLOW_EXTENSION = ".(jpeg|jpg|JPG|png|webp|bmp|gif)$";
const TARGET_PATTERN = "./**/*.{jpeg,jpg,JPG,webp,png,bmp,gif}";
const generateSnippets = (img, fileType) => {
const { replacedImgPath, replacedWebpPath, width, height } = img;
if (fileType === 'html') {
return `
<picture>
<source srcset="${replacedWebpPath}" type="image/webp" width="${width}" height="${height}" />
<img src="${replacedImgPath}" alt="" width="${width}" height="${height}" />
</picture>
`;
} else if (fileType === 'pug') {
return `
picture
source(srcset="${replacedWebpPath}" type="image/webp" width="${width}" height="${height}")
img(src="${replacedImgPath}" alt="" width="${width}" height="${height}")
`;
}
}
const sliceByNumber = (array, number) => {
const length = Math.ceil(array.length / number);
return new Array(length).fill().map((_, i) => array.slice(i * number, (i + 1) * number));
};
const generateSnippetsHandler = (fileType) => {
glob(TARGET_PATTERN, (err, files) => {
if (err) {
console.log(err); return;
}
const fileDimentions = files.map((file) => {
let dimentions = sizeOf(file);
dimentions.fileName = file;
return dimentions;
})
const slicedFileDimentions = sliceByNumber(fileDimentions, 2);
const imgValues = slicedFileDimentions.map((item) => {
const isExistWebp = item[1] || null;
const targetPatternDefault = ALLOW_EXTENSION.includes(item[0].type)
const imgPath = targetPatternDefault ? item[0].fileName : ''; // 空だったら、fileNameは""
let webpPath = "";
if (isExistWebp) {
const targetPatternWebp = item[1].type === 'webp';
webpPath = targetPatternWebp ? item[1].fileName : ''; // 空だったら、fileNameは""
} else {
console.log('webpファイルが存在しない画像ファイルがあります。\nファイルパスが空のsourceタグを生成します。');
}
const width = item[0].width;
const height = item[0].height;
// コンパイル後の形式にパス変換
const replacedImgPath = imgPath.replace('./src/', '/');
const replacedWebpPath = webpPath.replace('./src/', '/');
return {
replacedImgPath, replacedWebpPath, width, height
};
})
const resultSource = imgValues.map((item) => {
return generateSnippets(item, fileType);
})
const outPutDir = (type) => {
const OUTPUT_DIR = 'dist';
fs.mkdir(OUTPUT_DIR, { recursive: true }, (err) => {
if (err) { throw err; }
});
let fileExt;
if (type === 'html') {
fileExt = ".html";
} else if (type === 'pug') {
fileExt = ".pug";
}
return `./${OUTPUT_DIR}/snippets${fileExt}`;
}
const snippets = resultSource.join('');
fs.writeFile(outPutDir(fileType), snippets, function (err) {
if (err) {
console.error('エラーが発生しました。スニペットを生成できませんでした。');
throw err;
} else {
console.log(`\n${fileType}:スニペットが生成されました`);
}
});
});
}
const inputFileType = process.argv[2];
if (inputFileType === 'html' || inputFileType === 'pug') {
generateSnippetsHandler(inputFileType);
} else {
console.log("'html' または 'pug' のいずれかを入力してください。");
}
工夫したところ
glob を使ったファイル検知 / サイズ情報取得
const TARGET_PATTERN = "./**/*.{jpeg,jpg,JPG,png,bmp,gif}";
...
glob(TARGET_PATTERN, (err, files) => {
if (err) {
console.log(err); return;
}
const fileDimentions = files.map((file) => {
let dimentions = sizeOf(file);
dimentions.fileName = file;
return dimentions;
})
...
対象ファイルの情報を見にいくにあたり、今回はnpmパッケージのglob
を使用しております。
globの第一引数にはエントリーポイントとなるパスを渡す必要があります。
今回は、プロジェクト配下に存在する全ての画像ファイルを見にいくようワイルドカード(**)
を使用しております。
こうすることで多階層のディレクトリも含め全ての画像ファイルを検知することができるようになります。
またglobで収集したファイルパスから別途npmパッケージであるimage-size
というパッケージを用いることで、各々の画像ファイルよりサイズ情報などの情報を取得することができます。
pug のスニペット生成に対応
const generateSnippet = (img, fileType) => {
const { replacedImgPath, replacedWebpPath, width, height } = img;
if (fileType === 'html') {
return `
<picture>
<source srcset="${replacedWebpPath}" type="image/webp" width="${width}" height="${height}" />
<img src="${replacedImgPath}" alt="" width="${width}" height="${height}" />
</picture>
`;
} else if (fileType === 'pug') {
return `
picture
source(srcset="${replacedWebpPath}" type="image/webp" width="${width}" height="${height}")
img(src="${replacedImgPath}" alt="" width="${width}" height="${height}")
`;
}
}
...
const generateFile = (type) => {
const OUTPUT_DIR = 'dist';
let fileExt;
fs.mkdir(OUTPUT_DIR, { recursive: true }, (err) => {
if (err) { throw err; }
});
if (type === 'html') {
fileExt = ".html";
} else if (type === 'pug') {
fileExt = ".pug";
}
return `./${OUTPUT_DIR}/snippet${fileExt}`;
}
当初はhtmlタグの生成だけの実装になっていたのですが、
普段の制作現場ではpugを用いることが多いので、pugファイルの生成にも対応できるようにしました。
上のコードでは
-
generateSnippets
関数にて、関数に渡ってきた引数を見て適切なスニペットコードを生成。- 第1引数にはglobで収集した画像情報が入ってきます。
-
outputDir
関数に渡ってきた引数を見て、最終的なアウトプットをhtml
かpug
かを識別するようにしております。
おわりに
ファイル操作系のスクリプトにおいて、glob
とimage-size
は汎用性があって万能なのではないでしょうか。
作っておいてなんですが、結構必要とする場面がマニアックなので、需要があったら嬉しいな〜という感じです。笑
こちらに書ききれなかった内容は別途リポジトリを除いていただければと思います🙏
他の方の一助になれれば幸いです!🙇♂️
Discussion