レイアウトシフト対策としてimgタグのwidthとheightは設定した方がよいですが、サイズを確認して手入力だと手間がかかります。
色々調べてみると、image-sizeというパッケージで画像ファイルのサイズ情報を取得できるようだったので、このパッケージを使ってPugでのwidthとheightの自動付与までの処理を実装してみます。
使用する開発環境は、以前投稿したgulpfile.jsをES Modulesでの記述に移行した内容がベースになります。
開発環境
開発環境は以下のような構成になっています。
-
src/
- pug/
- sass/
- htdocs/
-
task/
- _config.mjs
- pug.mjs
- sass.mjs
- server.mjs
- watch.mjs
- gulpfile.mjs
- package.json
設定方法
今回の実装内容は以下になります。
- 画像のサイズ管理用のjsonファイルを追加
- 画像を特定のディレクトリに追加した際(今回の場合はhtdocs)、jsonの内容を更新
- Pug側でjsonのデータを使用できるように変数として渡す
- Gulp側でPug上で使用する画像パスを置換する関数を作成
- Pug側で上記変数と関数を使い、widthとheightを自動付与するmixinを作成
まずは画像サイズ管理用のjsonを作成します。
srcディレクトリ内にdataフォルダを作成して、その中にimgdata.jsonを空の内容で追加します。
次にこのjsonの更新を行うタスクを追加します。
使用するパッケージをインストールします。
npm install image-size globule --save-dev
taskディレクトリ内の_config.mjsに、今回追加する処理で使用するパスを追加します。
export const src = { root: 'src/', pug: ['src/pug/**/*.pug', '!src/pug/**/_*.pug'], pugbase: 'src/pug', pugwatch: 'src/pug/**/*.pug', sass: ['src/sass/**/*.scss', '!src/sass/**/_*.scss'], sasswatch: 'src/sass/**/*.scss', imgsizewatch: 'htdocs/**/*.{jpg,png,gif,svg,webp}', imgsizejson: 'src/data/imgdata.json' }; export const dest = { rootdir: 'htdocs', root: 'htdocs/', pug: 'htdocs/', css: 'htdocs/assets/css/' };
拡張子に.avifも含めようかと思ったのですが、記事作成時点でimage-sizeのサポートフォーマットに含まれていないようだったので外しています。
taskディレクトリ内にimgsize.mjsを追加して、jsonを更新する処理を記載します。
import fs from 'fs'; import { promisify } from 'util'; import globule from 'globule'; import imageSize from 'image-size'; import { src, dest } from "./_config.mjs"; const readFileAsync = promisify(fs.readFile); export async function imgsize_task(cd) { // 画像ファイルのパスを取得 const basePaths = globule.find({ src: [src.imgsizewatch] }); // 画像情報を取得 let updateData = []; for await (const imgPath of basePaths) { const buffer = await readFileAsync(imgPath); const imgData = imageSize(buffer); imgData.filepath = imgPath.replace(dest.rootDir, ''); updateData.push(imgData); } // jsonの内容を更新 const updateJSON = JSON.stringify(updateData); fs.writeFileSync(src.imgsizejson, updateJSON); cd(); }
これでjsonを更新する処理ができたので、gulpfile.mjsとtask/watch.mjsに追加します。
まずはgulpfile.mjsです。
import gulp from 'gulp'; import { pug_task } from "./task/pug.mjs"; import { sass_task } from "./task/sass.mjs"; import { server_task } from "./task/server.mjs"; import { watch_task } from "./task/watch.mjs"; import { imgsize_task } from "./task/imgsize.mjs"; export default gulp.parallel(watch_task, server_task); export {pug_task as pug}; export {sass_task as sass}; export {server_task as server}; export {watch_task as watch}; export {imgsize_task as imgsize};
次にtask/watch.mjsに追加して、画像追加時に自動でjsonが更新されるようにします。
import gulp from 'gulp'; import { src, dest } from "./_config.mjs"; import { pug_task } from "./pug.mjs"; import { sass_task } from "./sass.mjs"; import { imgsize_task } from "./imgsize.mjs"; export const watch_task = () => { gulp.watch(src.pugwatch, pug_task); gulp.watch(src.sasswatch, sass_task); gulp.watch(src.imgsizewatch, imgsize_task); }
これで開発環境起動後にhtdocs内に画像を追加すると、以下のような形でjsonの内容が更新されるようになりました。
[{"height":533,"width":800,"type":"jpg","filepath":"/assets/img/image1.jpg"},{"height":400,"orientation":1,"width":400,"type":"jpg","filepath":"/assets/img/image2.jpg"}]
このjsonの内容をPugで使用できるように設定を追加します。
task/pug.mjsの内容を変更します。
import fs from 'fs'; import gulp from 'gulp'; import plumber from 'gulp-plumber'; import pug from 'gulp-pug'; import browserSync from 'browser-sync'; import { src, dest } from "./_config.mjs"; export const pug_task = () => { // 画像サイズ管理用のデータを取得 const bufferData = fs.readFileSync(src.imgsizejson); const dataJSON = bufferData.toString(); const imgData = JSON.parse(dataJSON); return gulp.src(src.pug) .pipe(plumber()) .pipe(pug({ pretty: true, basedir: src.pugbase, locals: { imgData } })) .pipe(gulp.dest(dest.pug)) .pipe(browserSync.reload({stream:true})); }
これでimgDataという変数でjsonのデータを使用できるようになりました。
imgのsrcを元にjsonデータ内の探索を行うのですが、srcの書き方は複数パターンあり得るので、jsonデータの探索用に画像パスを置換する関数を作成します。
task/pug.mjs内に追加します。
import fs from 'fs'; import gulp from 'gulp'; import plumber from 'gulp-plumber'; import pug from 'gulp-pug'; import browserSync from 'browser-sync'; import { src, dest } from "./_config.mjs"; export const pug_task = () => { // 画像サイズ管理用のデータを取得 const bufferData = fs.readFileSync(src.imgsizejson); const dataJSON = bufferData.toString(); const imgData = JSON.parse(dataJSON); // ファイルパスの調整用関数 const pathAdjust = (src, currentDir) => { // htdocsを起点にしたルートパスに変換 let reSrc; // ルートパスの場合はそのまま if (src.startsWith('/')) { reSrc = src; // 相対パス(「./」か「../」から始まる)の場合 } else if (src.startsWith('./') || src.startsWith('../')) { reSrc = new URL(src, `https://example.com${currentDir}`).pathname; // 絶対パス(「http」から始まる)の場合 } else if (src.startsWith('http')) { reSrc = new URL(src).pathname; // ディレクトリ名もしくはファイル名から始まる場合 } else { reSrc = new URL(src, `https://example.com${currentDir}`).pathname; } return reSrc; } return gulp.src(src.pug) .pipe(plumber()) .pipe(pug({ pretty: true, basedir: src.pugbase, locals: { imgData, pathAdjust } })) .pipe(gulp.dest(dest.pug)) .pipe(browserSync.reload({stream:true})); }
相対パスの場合は画像パスのみだと現在どこにいるのかがわからないので、現在のディレクトリまでのルートパスを合わせて渡す想定にしています。
最終的にはルートパスにするのですが、処理の都合上ドメインを含めたURLにする必要があるので、一時的に「https://example.com」を付与する形にしています。
このままでも大丈夫ですが、実際に使用する際は案件のドメインなどに変更するでも問題ありません。
絶対パス(httpsまたはhttp始まり)の場合に関してもルートパスに変換するため、htdocsに含まれる画像サイズを使用する形になります。
これで一通りの準備ができたので、Pug側にmixinを作成します。
//- options { src: '画像パス', alt: 'altテキスト', currentDir: '現在のディレクトリパス(画像パスがルートパスでない場合に使用)' } mixin img(options) //- オプションが設定されていない場合の初期設定 - var options = options||{}; - var src = options.src?options.src:''; - var alt = options.alt?options.alt:''; - var currentDir = options.currentDir?options.currentDir:''; //- 画像パスをルートパスの形に調整 - var rootPath = pathAdjust(src, currentDir) //- jsonの画像データから一致するファイルデータを探す - for (var x = 0; x < imgData.length; x++) if(imgData[x].filepath == rootPath) - var width = imgData[x].width - var height = imgData[x].height img(src=src width=width height=height alt=alt #{attrs})
このmixinを使っていくつかのパターンでテストしてみます。
ファイル構成は以下のようになっています。
-
src/
- data/imgdata.json
- pug/company/about/index.pug
-
htdocs/
-
assets/img/
- image1.jpg
- image2.jpg
-
company/about/
-
img/
- photo01.jpg
- photo02.jpg
- index.html
- ogp.jpg
-
img/
-
assets/img/
json内には各画像の情報が更新されています。
[{"height":533,"width":800,"type":"jpg","filepath":"/assets/img/image1.jpg"},{"height":400,"orientation":1,"width":400,"type":"jpg","filepath":"/assets/img/image2.jpg"},{"height":1066,"width":1600,"type":"jpg","filepath":"/company/about/img/photo01.jpg"},{"height":533,"width":800,"type":"jpg","filepath":"/company/about/img/photo02.jpg"},{"height":1066,"orientation":1,"width":1600,"type":"jpg","filepath":"/company/about/ogp.jpg"}]
src/pug/company/about/index.pug で以下を追加します。
// 引数未設定 +img() // ルートパス + alt +img({ src: '/assets/img/image1.jpg', alt: '画像1' }) // ルートパス(存在しないファイル) +img({ src: '/assets/img/image5.jpg', currentDir: '/company/about/' }) // 相対パス1 +img({ src: '../../assets/img/image2.jpg', currentDir: '/company/about/' }) // 相対パス2 +img({ src: './img/photo01.jpg', currentDir: '/company/about/' }) // 相対パス3 +img({ src: 'img/photo02.jpg', currentDir: '/company/about/' }) // 相対パス4 +img({ src: 'ogp.jpg', currentDir: '/company/about/' })
コンパイル後は以下のようになり、意図した通りwidthとheightの値を自動付与できました。
<!-- 引数未設定--> <img src="" alt=""/> <!-- ルートパス + alt--> <img src="/assets/img/image1.jpg" width="800" height="533" alt="画像1"/> <!-- ルートパス(存在しないファイル)--> <img src="/assets/img/image5.jpg" alt=""/> <!-- 相対パス1--> <img src="../../assets/img/image2.jpg" width="400" height="400" alt=""/> <!-- 相対パス2--> <img src="./img/photo01.jpg" width="1600" height="1066" alt=""/> <!-- 相対パス3--> <img src="img/photo02.jpg" width="800" height="533" alt=""/> <!-- 相対パス4--> <img src="ogp.jpg" width="1600" height="1066" alt=""/>
機能追加
imgタグに対してclassやloading属性、data属性など他属性を付与したいということが多そうなので、他属性も設定できるように機能を追加してみます。
前の実装ではPug側でjson内の探索とimgタグの生成を行っていましたが、Pug側で複数の属性を自由に設定できるようにするのが難しそうだったので、Gulp側でimgタグの生成までを行った上でPug側に返すように変更します。
import fs from 'fs'; import path from 'path'; import gulp from 'gulp'; import plumber from 'gulp-plumber'; import pug from 'gulp-pug'; import browserSync from 'browser-sync'; import { src, dest } from "./_config.mjs"; export const pug_task = () => { // 画像サイズ管理用のデータを取得 const bufferData = fs.readFileSync(src.imgsizejson); const dataJSON = bufferData.toString(); const imgData = JSON.parse(dataJSON); // ファイルパスの調整用関数 const pathAdjust = (src, currentDir) => { // htdocsを起点にしたルートパスに変換 let reSrc; // ルートパスの場合はそのまま if (src.startsWith('/')) { reSrc = src; // 相対パス(「./」か「../」から始まる)の場合 } else if (src.startsWith('./') || src.startsWith('../')) { reSrc = new URL(src, `https://example.com${currentDir}`).pathname; // 絶対パス(「http」から始まる)の場合 } else if (src.startsWith('http')) { reSrc = new URL(src).pathname; // ディレクトリ名もしくはファイル名から始まる場合 } else { reSrc = new URL(src, `https://example.com${currentDir}`).pathname; } return reSrc; } // imgタグ生成用関数 const setImgSize = (src, alt, currentDir, attr) => { // 画像パスをルートパスの形に調整 const rootPath = pathAdjust(src, currentDir); // 画像に設定する属性の準備 const d = imgData.find((e) => e.filepath === rootPath); let widthAttr = ` width="${d?.width}"`; if(!d?.width) widthAttr = ''; let heightAttr = ` height="${d?.height}"`; if(!d?.height) heightAttr = ''; let altAttr = alt; if(altAttr == null) altAttr = ''; let otherAttr = ''; for(const [key, value] of Object.entries(attr)) { otherAttr += ` ${key}="${value}"`; } return `<img src="${src}"${widthAttr}${heightAttr} alt="${altAttr}"${otherAttr}>`; } return gulp.src(src.pug) .pipe(plumber()) .pipe(pug({ pretty: true, basedir: src.pugbase, locals: { setImgSize } })) .pipe(gulp.dest(dest.pug)) .pipe(browserSync.reload({stream:true})); }
setImgSize関数のattrという引数に、オブジェクト形式で他属性を設定する想定です。
次にmixinの変更です。
//- options { src: '画像パス', alt: 'altテキスト', currentDir: '現在のディレクトリパス(画像パスがルートパスでない場合に使用)', attr: { // その他設定したい属性(未実装) 'class': 'クラス名', 'id': 'ID名', 'data-xxx': 'データ属性', etc... } } mixin img(options) //- オプションが設定されていない場合の初期設定 - var options = options||{}; - var src = options.src?options.src:''; - var alt = options.alt?options.alt:''; - var currentDir = options.currentDir?options.currentDir:''; - var attr = options.attr||{}; //- サイズをセットした画像タグの生成 - var imgTag = setImgSize(src, alt, currentDir, attr) != imgTag
Gulpから返ってきたimgタグをエスケープしないように「!=」で出力しています。
属性の追加をいくつか試してみます。
// 引数未設定 +img() // ルートパス + alt +img({ src: '/assets/img/image1.jpg', alt: '画像1', attr: { class: 'img1', id: 'img1' } }) // ルートパス(存在しないファイル) +img({ src: '/assets/img/image5.jpg', currentDir: '/company/about/' }) // 相対パス1 +img({ src: '../../assets/img/image2.jpg', currentDir: '/company/about/', attr: { 'class': 'img-test', loading: 'lazy' } }) // 相対パス2 +img({ src: './img/photo01.jpg', currentDir: '/company/about/', attr: { 'data-abc': 'def' } }) // 相対パス3 +img({ src: 'img/photo02.jpg', currentDir: '/company/about/', attr: { 'title': 'タイトル' } }) // 相対パス4 +img({ src: 'ogp.jpg', currentDir: '/company/about/', attr: { 'aria-label': 'ラベル' } })
コンパイルすると、設定した属性の追加が確認できました。
<!-- 引数未設定--> <img src="" alt=""> <!-- ルートパス + alt--> <img src="/assets/img/image1.jpg" width="800" height="533" alt="画像1" class="img1" id="img1"> <!-- ルートパス(存在しないファイル)--> <img src="/assets/img/image5.jpg" alt=""> <!-- 相対パス1--> <img src="../../assets/img/image2.jpg" width="400" height="400" alt="" class="img-test" loading="lazy"> <!-- 相対パス2--> <img src="./img/photo01.jpg" width="1600" height="1066" alt="" data-abc="def"> <!-- 相対パス3--> <img src="img/photo02.jpg" width="800" height="533" alt="" title="タイトル"> <!-- 相対パス4--> <img src="ogp.jpg" width="1600" height="1066" alt="" aria-label="ラベル">
コメントが承認されるまで時間がかかります。