サーバーにある画像のサイズや更新日時を取得する【JavaScript/非同期/Promise】

JavaScript

サーバーの画像サイズや更新日を取得して表示する機会があったので、実装方法を調べました。
忘れないようにメモしておきます!

画像のサイズを取得する

次のように、画像の下にサイズを表示することを想定しています。

実際の表示はこちらで確認できます!
※画像はLorem Picsumというダミー画像を挿入できるサービスを使用しています。

See the Pen img-size by donguri2020 (@m-ke) on CodePen.

コード(全体)

まず、コード全体を確認します。
例えば、次のimgタグの画像のサイズを#imgInfoに表示したい場合

HTML

<div id="imgArea">
  <img id="targetImg" src="<画像のURL>" />
  <p id="imgInfo"></p> <!-- #imgInfoに画像サイズを表示させる -->
</div>

次のJavaScriptで画像サイズを取得できました。

JavaScript

(async () => {
  // 要素を取得
  const targetImg = document.querySelector("#targetImg");
  const imgInfo =  document.querySelector("#imgInfo");
  
  // 画像データ取得
  const ImgData = await getImgElm(targetImg);
  // 画像サイズを表示
  imgInfo.textContent = "サイズ:" + ImgData;
})();

// 画像データ取得
function getImgElm(target) {
  const imgElm = target;

  // 画像の読み込み完了まで待つ
  return new Promise(function (resolve) {
      imgElm.addEventListener("load", function () {
        const width = imgElm.naturalWidth;
        const height = imgElm.naturalHeight;
        resolve(`${width}×${height}px`);
    })
  });
}

画像サイズを取得するまでのパターン色々

画像サイズはHTMLImageElementのnaturalWidthnaturalHeightプロパティで参照できます。
これらのプロパティは読み取り専用で、画像本来の大きさをピクセルで返します。

画像サイズの取得で、いくつかパターンがあったのでメモしておきます。

画像サイズを取得する基本的な使い方

まず、もっともシンプルで基本的な使い方は次の通りです。

<img id="targetImg" src="test-1200×890.jpg">

<script>
  const targetImg = document.querySelector("#targetImg");
  const width = targetImg.naturalWidth;
  const height = targetImg.naturalHeight;
  
  console.log(`${width}×${height}px`);   // 1200×890px
</script>

これだけで画像本来のサイズが取得できます。

画像が読み込まれたらサイズを取得する(非同期なし)

画像1枚のサイズを表示するだけなら、先程のコードでもうまく動くと思いますが、読み込みのタイミングによってはうまく取得できない時もあります。

naturalWidthnaturalHeightプロパティは、画像を読み込まないと値がうまく取得できないようです。

先程のコードを、画像が読み込まれたらサイズを取得するように変更します。

<img id="targetImg" src="test-1200×890.jpg">

<script> 
  const targetImg = document.querySelector("#targetImg")

  // 画像の読み込みまで待つ
    targetImg.addEventListener("load", function () {
      const width = targetImg.naturalWidth;
      const height = targetImg.naturalHeight;
      console.log(`${width}×${height}px`);   // 1200×890px
    })
</script>

イベントリスナーloadで、該当のイメージ読み込まれたら画像サイズを取得します。

画像の読み込みが完了したらサイズを取得する(非同期あり)

先程のコードでも画像サイズを取得できると思いますが、そもそも画像を生成する構造になっていたり、取得した画像サイズを関数で戻り値にした場合、うまくサイズを取得できない時があります。

画像読み込みの完了を待たずに、次の処理に移行するようだと、画像サイズがうまく取得できないようです。

そういった時は、Promiseオブジェクトで非同期処理にします。

<img id="targetImg" src="test-1200×890.jpg">

<script> 
  const targetImg = document.querySelector("#targetImg")

  // 画像の読み込み完了まで待つ
  new Promise(function (resolve) {
    targetImg.addEventListener("load", function () {
      const width = targetImg.naturalWidth;
      const height = targetImg.naturalHeight;
      resolve(`${width}×${height}px`);
    })
  }).then((imgSize) => console.log(imgSize))   // 1200×890px
</script>

すぐに値を返せない処理の時、処理が終わったら最終的に何かしらの値を返してくれるのが非同期処理です。

ここでは、画像の読み込みの完了を待ってからコンソールに画像サイズを出力することになります。

具体的には、イベントリスナーloadで読み込まれた画像サイズをresolve()に渡します。そうすると、then()の引数にresolve()で渡した画像サイズが格納されます。
結果的には画像の読み込みの完了を待ってから処理を実行することになります。

これで画像のサイズが取得できました!

画像の更新日時を取得する

次のように、画像の下に更新日時を表示することを想定しています。

実際の表示はこちらで確認できます!
※画像はLorem Picsumというダミー画像を挿入できるサービスを使用しています。

See the Pen img-modified by donguri2020 (@m-ke) on CodePen.

コード(全体)

まず、コード全体を確認します。
例えば、次のimgタグの画像の更新日を#imgInfoに表示したい場合

HTML

<div id="imgArea">
  <img id="targetImg" src="<画像のURL>" />
  <p id="imgInfo"></p> <!-- #imgInfoに更新日を表示させる -->
</div>

次のJavaScriptで更新日を取得できました。

JavaScript

(async () => {
  // 要素を取得
  const targetImg = document.querySelector("#targetImg");
  const imgInfo =  document.querySelector("#imgInfo");
  
  // 画像データを非同期で取得
  const fetchImg = await fetchImage(targetImg.src);
  // 更新日時を取得
  const modified = await getModified(fetchImg);
  // 更新日時を表示
  imgInfo.textContent = "更新日時:" + formatDate(modified)
})();

// fetch(非同期)
async function fetchImage(target) {
  const response = await fetch(target);
  if (response.ok) {
    return response;
  } else {
      throw new Error('The data could not be read');
   }
}

// 更新日取得
function getModified(target) {
  const headers = target.headers;
  let lastModified = "";
  // 更新日時を取得(GMT)
  for (var pair of headers.entries()) {
    console.log(pair)
    if (pair[0] === "last-modified") {
      lastModified = pair[1];
    }
  }
  return lastModified;
}

// GMTを年月日分に変換
function formatDate(targetDate) {
  const modified = new Date(targetDate);
  const year = modified.getFullYear();
  const month = modified.getMonth() + 1;
  const date = modified.getDate();
  const hours = modified.getHours();
  const minutes = modified.getMinutes();
  return +year + "年" + month + "月" + date + "日" + hours + "時" + minutes + "分";
};

更新日時を取得するまでのポイント

更新日時を取得する際、難しかったところをメモしておきます。

まず、更新日時はHTTPレスポンスのHTTPヘッダーに含まれています。
HTTPヘッダーは、サーバーから返却されるデータの状態が含まれています。

そこでHTTPリクエストをサーバーに送って、HTTPレスポンスのデータを取得するところから始めます。

参考サイト

HTTPリクエスト/レスポンスとは? HTTPヘッダーを理解しよう | 初代編集長ブログ―安田英久
HTTPは「HTTPリクエスト」と「HTTPレスポンス」に分けて考えます。これらは一体どのようなものなのか? よく耳にするリファラー(Referer)やクッキー(Cookie)などはどんな働きをしているのか? HTTPヘッダーを理解しておくとサイト上での仕組み作りの視野が広がるかもしれません。また、知っておくと役に立つ...

fetch APIでレスポンスを非同期で取得する

fetch APIは指定したURL(今回は画像のURL)にHTTPリクエストを送信します。その結果として、サーバーからHTTPレスポンスのデータを取得できます。

なお、fetch APIの戻り値はPromiseオブジェクト(非同期)になります。

非同期とは、先ほど画像サイズの取得でも出てきましたが、すぐに値は返せないけど、最終的に何かしらの値を返すよってやつです。ざっくりな理解ですが、、。

ここではHTTPリクエストをサーバーに送って、HTTPレスポンスの値が返却されるのを待ち、最終的には何かしら値を返すよってことになります。

非同期の処理が終わり、HTTPレスポンスを取得できた場合、その値はthen()の引数に格納されます。
以下は、サーバーから返却されたHTTPレスポンスのデータをconsole.log()に出力するサンプルです。

fetch(<画像のURL>).then((response) => console.log(response))

コンソールに表示されたデータをキャプチャしました。ページ本体であるbodyやurl、データ取得の成否など、さまざまな値が確認できます。

今回取得したいのはファイルの更新日時です。
更新日時はHTTPヘッダーに格納されています。具体的には、headersプロパティのHeadersオブジェクトに格納されているので、値を取り出していきます。

Headers.entries()で更新日時を取り出す

先ほど取得したheadersプロパティの値、Headersオブジェクトに更新日時が含まれるので、Headers.entries()でデータを取り出します。

Headers.entries()は、Headersオブジェクトに含まれるすべてのキーと値のペアを取り出すことができます。

構文は次の通りです。

// Headersオブジェクト
const headers = new Headers();

// キーと値のペアを取得
for (const pair of headers.entries()) {
   console.log(`${pair[0]}: ${pair[1]}`);
}

Headers.entries()で取り出した値をコンソールで表示させたデータをキャプチャしました。

配列の1番目の値last-modifiedに更新日時が格納されています。
これを条件にしてfor文とif文で更新日時だけを取り出します。

  // 更新日時を取得(GMT)
  for (var pair of headers.entries()) {
    if (pair[0] === "last-modified") {
      lastModified = pair[1];
    }
  }

参考サイト

Headers.entries() - Web API | MDN
Headers.entries() メソッドは、このオブジェクトに含まれるすべてのキーと値のペアを走査する反復子を返します。それぞれのペアのキーと値は両方とも String オブジェクトです。

更新日時をGTMから年月日時分の表記に変換する

Headers.entries()で取得した更新日時は、次のようなフォーマットになっています。

Fri, 07 Oct 2022 09:24:46 GMT

これはグリニッジ標準時という表記らしいです。
このままだと見づらいので、以下の関数で年月日時分の表記に変換します。

// GMTを年月日分に変換
function formatDate(targetDate) {
  const modified = new Date(targetDate);
  const year = modified.getFullYear();
  const month = modified.getMonth() + 1;
  const date = modified.getDate();
  const hours = modified.getHours();
  const minutes = modified.getMinutes();
  return +year + "年" + month + "月" + date + "日" + hours + "時" + minutes + "分";
};

場合によっては時間が9時間ずれるらしいので、その時はgetDate()の値を調整すればよさそうです。

エラー処理を追加して async / await に書き換える

これで更新日時を取得できますが、最後に、コードを整理します。
非同期の箇所をasync / awaitで書き換えたり、エラーが発生した時の処理を追加します。

関係ある箇所を抜き出しました。

(async () => {
  // 画像データを非同期で取得
  const fetchImg = await fetchImage(<画像のURL>);
})();

// fetch(非同期)
async function fetchImage(target) {
  const response = await fetch(target);
 // データ取得可否
  if (response.ok) {
    return response;
  } else {
      throw new Error('The data could not be read');
   }
}

これで画像の更新日時が取得できました!

サーバーの画像を取得して一覧表示する(サンプル)

最後に、実際に使えそうなシチュエーションでサンプルを作りました。
サーバーの画像情報を取得して一覧にします。

実際の表示はこちらで確認できます!

コード(全体)

ポイントとなる箇所をメモしておきます。
まず、サーバーの画像取得といっても、直接サーバーにどんな画像があるか見に行っているわけではなく、画像のURLを配列で準備し、それを元に画像一覧を作成します。

imgData.js

これが配列で用意した画像のURLです。exportして、他のファイルでも読み込めるようにします。
404エラーで画像が表示されない場合を想定して、無効なデータを含めました。

export const imgData = [
  "src/images/photo01.jpg",
  "src/images/photo02.jpg",
  "src/images/photo03.jpg",
  "src/images/データがないよ",
  "src/images/photo04.jpg",
  "src/images/photo05.jpg",
  "src/images/photo06.jpg",
  "src/images/photo07.jpg",
  "src/images/photo08.jpg",
  "src/images/photo09.jpg",
  "src/images/photo10.jpg",
  "src/images/photo11.jpg",
  "src/images/photo12.jpg",
  "src/images/photo13.jpg",
  "src/images/photo14.jpg",
  "画像データなし",
  "src/images/photo15.jpg",
  "src/images/photo16.jpg",
  "src/images/photo17.jpg",
  "src/images/photo18.jpg",
  "src/images/photo19.jpg",
  "src/images/photo20.jpg",
  "src/images/photo21.jpg",
  "src/images/photo22.jpg",
  "src/images/photo23.jpg",
  "src/images/photo25.jpg",
  "src/images/photo26.jpg"
];

index.html

htmlはシンプルです。#imgAreaにJavaScriptで生成した画像の一覧が読み込まれます。

<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="imgArea"></div>
    <script src="src/index.js"></script>
  </body>
</html>

JavaScript

こちらがJavaScript全体です。
先ほどと異なり、画像が取得できなかった場合の処理も追加しています。

import { imgData } from "./imgData";
import { formatDate } from "./modules/formatDate";
import "./styles.css";

// // 画像一覧を表示
(async () => {
  for (const imgURL of imgData) {
    try {
      const fetchImg = await fetchImage(imgURL);
      const ImgData = await getImgElm(fetchImg); // 画像データ取得
      const modified = await getModified(fetchImg); // 更新日取得

      // 要素生成
      createImg(ImgData, modified);
    } catch (e) {
      console.error(e);
    }
  }
})();

// fetch
async function fetchImage(target) {
  const response = await fetch(target);
  if (response.ok) {
    return response;
  }
}

// 画像データ取得
function getImgElm(target) {
  const imgElm = document.createElement("img");
  imgElm.src = target.url;

  // 画像の読み込み完了まで待つ
  return new Promise(function (resolve, reject) {
    let loadImgFlg = true;

    // 画像が取得できなかった場合
    imgElm.addEventListener("error", function () {
      loadImgFlg = false;
      imgElm.src = "src/images/noimage.jpg";
      resolve({ imgElm, width: "-", height: "-", loadImgFlg });
    });
    // 画像を取得できた場合
    if (loadImgFlg) {
      imgElm.addEventListener("load", function () {
        const width = imgElm.naturalWidth;
        const height = imgElm.naturalHeight;
        resolve({ imgElm, width, height, loadImgFlg });
      });
    }
  });
}

// 更新日取得
async function getModified(target) {
  const headers = target.headers;
  let lastModified = "";
  // 更新日時を取得(GMT)
  for (var pair of headers.entries()) {
    if (pair[0] === "last-modified") {
      lastModified = pair[1];
    }
  }
  return lastModified;
}

// 要素生成
async function createImg(targetImg, targetModified) {
  const imgArea = document.getElementById("imgArea");
  const imgElm = targetImg.imgElm;
  const divElm = document.createElement("div");
  const pElm = document.createElement("p");
  const spanElm = document.createElement("span");
  spanElm.textContent = `サイズ:${targetImg.width} × ${targetImg.height}`;

  // 画像の読み込み完了で更新日時の表示を分ける
  if (targetImg.loadImgFlg) {
    pElm.textContent = `更新日時:${formatDate(targetModified)}`;
  } else {
    pElm.textContent = `更新日時: -年-月-日`;
  }

  divElm.appendChild(imgElm);
  pElm.appendChild(spanElm);
  divElm.appendChild(pElm);
  imgArea.appendChild(divElm);
}

画像一覧取得のポイント

画像のURL配列からfor文でURLを1つずつ取り出し、fetch APIでHTTPレスポンスを取得します。
そこから更新日時を取得し、別途画像サイズも取得します。

先ほどとの違いは、そこから画像を生成し、ブラウザに表示していることです。

それぞれの処理が終わってからでないとエラーが発生するので、awaitで処理が完了してから実行しています。
※更新日時の変換処理は別途ファイルにまとめていますが、詳細はCode Sandboxで確認できます。

まとめ

画像のサイズはプロパティを参照するだけで取得できると思いきや、読み込みのタイミングで思ったような挙動にならなかったりしました。
更新日時も結局はHTTPリクエストが必要だったりと思ったより手間でした。

今回、画像情報の取得に関してある程度まとめたので、今後は自分でこのページを参考にします!