JavaScript で、画像本来のサイズ(幅, 高さ)を取得する方法

Opera で DOM Mutation Event を使用するコードを追記しました。
まとめを追加しました。
Opera パート2に取得できないケースが見つかったため、パート3 を追加しました。

rhino

rhino.jpg(幅:300px, 高さ:227px) を、

<img id="rhino" src="rhino.jpg" width="100" height="75" />

と、100 x 75 で表示している場合を例に、画像本来のサイズを取得する方法をご紹介します。

Firefox, Safari, Google Chrome なら

image.naturalWidth と image.naturalHeight を利用します。
image.naturalWidth と image.naturalHeight の初期値は 0 です。画像の読み込みが完了した時点で適切な値に更新されます。

<html><head><title></title></head><body>
<img id="rhino" src="img/rhino.jpg" width="100" height="75" />
<script>
window.onload = function() {
  alert(rhino.width + " " + rhino.height); // "100 75"
  alert(rhino.naturalWidth + " " + rhino.naturalHeight); // "300 227"
}
</script></body></html>

IE なら

runtimeStyle を利用します。
runtimeStyle は CSS の !important をプログラマブルにエミュレートする機構です。
runtimeStyle.width と runtimeStyle.height を一時退避し、"auto" で上書することで本来のサイズを取得しています。
"auto" の代わりに "" でもよさそうに見えますが、"" だと期待した結果は得られません。
ページのリフローは発生しません(ガタガタ動きません)。

<html><head><title></title></head><body>
<img id="rhino" src="img/rhino.jpg" width="100" height="75" />
<input type="button" value="revalidate" onclick="boot()" />
<script>
function getActualDimension1(image) {
  var w, h, key = "actual", run, mem;

  if (image[key] && image[key].src === image.src) {
    return image[key];
  }

  run = image.runtimeStyle;
  mem = { w: run.width, h: run.height }; // keep runtimeStyle

  run.width  = "auto"; // override
  run.height = "auto";
  w = image.width;
  h = image.height;
  run.width  = mem.w; // restore
  run.height = mem.h;

  return image[key] = { width: w, height: h, src: image.src }; // bond
}
function boot() {
  var actual = getActualDimension1(rhino);
  alert(rhino.width + " " + rhino.height); // "100 75"
  alert(actual.width + " " + actual.height); // "300 227"
}
window.onload = boot;
</script></body></html>

Opera なら

Opera には、runtimeStyle が無く(currentStyle はある)、image.naturalWidth もないため、非同期で処理します。
img.onload = "";img = void 0;IE で発生するメモリリークを回避するためのコードです。Opera では顕著なメモリリークは発生しないようですが、このコード自体は Opera 以外のブラウザにも流用可能なため、一応やってます。

<html><head><title></title></head><body>
<img id="rhino" src="img/rhino.jpg" width="100" height="75" />
<input type="button" value="revalidate" />
<script>
function getActualDimension2(image, callback) {
  var img, delayLoader;

  delayLoader = function() {
    var actual = { width: img.width,
                   height: img.height };
    img.onload = "";
    img = void 0; // free obj
    callback(actual);
  };

  img = new Image();
  img.onload = delayLoader;
  img.src = image.src;
}

function boot() {
  var fn = function(actual) {
    alert(rhino.width + " " + rhino.height); // "100 75"
    alert(actual.width + " " + actual.height); // "300 227"
  };
  getActualDimension2(rhino, fn);
}
window.onload = boot;
</script></body></html>

Opera なら(パート2)


addEventListener("DOMAttrModified") で同期処理が可能です(id:edvakf さんアドバイス感謝です)
removeAttribute("width", "height"); がポイントです。rhino.width = なにか; のようにwidth, height に代入するやり方だとダメです。
ページのリフローは発生しません(ガタガタ動きません)。

パート2は、以下のように height が明示されていないHTMLでイベントが発生しないという弱点がありました。パート3 をご覧下さい。

<img id="rhino" src="rhino.jpg" width="100" />

<html><head><title></title></head><body>
<img id="rhino" src="img/rhino.jpg" width="100" height="75" />
<input type="button" value="revalidate" onclick="boot()" />
<script>
function getActualDimension3(image) {
  var w = 0, h = 0, mem, key = "actual";

  if (image[key] && image[key].src === image.src) {
    return image[key];
  }

  function fn() {
    w = image.width;
    h = image.height;
  }

  mem = { w: image.width, h: image.height };
  image.removeAttribute("width");
  image.addEventListener("DOMAttrModified", fn, false);
  image.removeAttribute("height");
  // call fn
  image.removeEventListener("DOMAttrModified", fn, false);
  image.width  = mem.w;
  image.height = mem.h;

  return image[key] = { width: w, height: h, src: image.src }; // bond
}

function boot() {
  var actual = getActualDimension3(rhino);
  alert(rhino.width + " " + rhino.height); // "100 75"
  alert(actual.width + " " + actual.height); // "300 227"
}
window.onload = boot;
</script></body></html>

Opera なら(パート3)

パート2 に取得できないケース(穴)が見つかったため、もう一度考え直してみました。
ものすごく簡単になりましたが、このようなコードでもページのリフローは発生しないようです(リフローが発生するようでしたら教えてください)。

<html><head><title></title></head><body>
<img id="rhino" src="img/rhino.jpg" width="100" height="75" />
<input type="button" value="revalidate" onclick="boot()" />
<script>
function getActualDimension3(image) {
  var w, h, mem, key = "actual";

  if (image[key] && image[key].src === image.src) {
    return image[key];
  }

  mem = { w: image.width, h: image.height };
  image.removeAttribute("width");
  image.removeAttribute("height");
  w = image.width;
  h = image.height;
  image.width  = mem.w;
  image.height = mem.h;

  return image[key] = { width: w, height: h, src: image.src }; // bond
}

function boot() {
  var actual = getActualDimension3(rhino);
  alert(rhino.width + " " + rhino.height); // "100 75"
  alert(actual.width + " " + actual.height); // "300 227"
}
window.onload = boot;
</script></body></html>

まめちしき

今回は利用していませんが、IE の runtimeStyle に似た document.getOverrideStyle が、DOM に存在します。

DOM: http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/guide/plugin/dom/org/w3c/dom/css/DocumentCSS.html
var css = document.getOverrideStyle(element, pseudoElement);

参考: http://archives.devshed.com/forums/standards-105/document-getoverridestyle-2200574.html

SafariGoogle Chrome でメソッドが実装されているようです。Opera ではエラーになります。

さがしてます


Opera で、image.naturalWidth や、runtimeStyle に代わる方法をご存知の方は教えてください。
# 予め画面外に <img id="hidden_rhino" src="rhino.jpg"> を設置しておいて、hidden_rhino からサイズを取得する方法以外で

まとめ

まとめるとこうなります。こちらで実際に試せます
Firefox2+, Google Chrome1+, Safari3+, Opera9.27+, IE6+ で動作を確認しました。

<html><head><title></title></head><body>
<img id="rhino1" src="img/rhino.jpg" width="100" height="75" />
<img id="rhino2" src="img/rhino.jpg" width="100" />
<img id="rhino3" src="img/rhino.jpg" height="75" />
<input type="button" value="revalidate" onclick="boot()" />
<div id="log"></div>
<script>
/* LICENSE: MIT
 * AUTHOR: uupaa.js@gmail.com
 */
function getActualDimension(image) {
  var run, mem, w, h, key = "actual";

  // for Firefox, Safari, Google Chrome
  if ("naturalWidth" in image) {
    return { width:  image.naturalWidth,
             height: image.naturalHeight };
  }

  if ("src" in image) { // HTMLImageElement
    if (image[key] && image[key].src === image.src) {
      return image[key];
    }
    if (document.uniqueID) { // for IE
      run = image.runtimeStyle;
      mem = { w: run.width, h: run.height }; // keep runtimeStyle
      run.width  = "auto"; // override
      run.height = "auto";
      w = image.width;
      h = image.height;
      run.width  = mem.w; // restore
      run.height = mem.h;
    } else { // for Opera and Other
/*
      function fn() {
        w = image.width;
        h = image.height;
      }
      mem = { w: image.width, h: image.height }; // keep current style
      image.removeAttribute("width");
      image.addEventListener("DOMAttrModified", fn, false);
      image.removeAttribute("height");
      // call fn
      image.removeEventListener("DOMAttrModified", fn, false);
      image.width  = mem.w; // restore
      image.height = mem.h;
 */
      mem = { w: image.width, h: image.height }; // keep current style
      image.removeAttribute("width");
      image.removeAttribute("height");
      w = image.width;
      h = image.height;
      image.width  = mem.w; // restore
      image.height = mem.h;
    }
    return image[key] = { width: w, height: h, src: image.src }; // bond
  }
  // HTMLCanvasElement
  return { width: image.width, height: image.height };
}
function boot() {
  var actual1 = getActualDimension(rhino1),
      actual2 = getActualDimension(rhino2),
      actual3 = getActualDimension(rhino3);
/*
  alert(rhino1.width + " " + rhino1.height); // "100 75"
  alert(actual1.width + " " + actual1.height); // "300 227"
 */
  log.innerHTML += [
    [rhino1.width, rhino1.height, actual1.width, actual1.height].join(", "),
    [rhino2.width, rhino2.height, actual2.width, actual2.height].join(", "),
    [rhino3.width, rhino3.height, actual3.width, actual3.height].join(", ")
  ].join(" / ") + "<br />";
}
window.onload = boot;
</script></body></html>