♪Vector Vector, please. Oh, the mess I'm in (c) UFO

IEのインラインSVGの固有サイズ表示をSVG1.1仕様に修正する方法

この記事はSVG Advent Calendar 2014 - Adventarの11日目の参加記事です。

実は、この記事のきっかけとなったのも、SVG Advent Calendarに投稿されていた@_tanshioさんの「IEにも対応したレスポンシブSVGの作り方」という記事でした。

記事を読んだとき、直感的に「IEの問題というよりもSVGやCSSの記述方法に問題がありそう」という印象を持ち、簡単にテストしてみてツイッターでつぶやいたら、ご本人からリプライがあっていろいろやりとりをしていました。 この記事は、その一連のやりとりをまとめなおしたものです。 ただ、自分の思い込みなどから見当違いなツイートがアレやコレやとあったりもしたので、この記事は、その一連のやりとりにおける自分の発言に対する訂正の意味合いも兼ねていますw

インラインSVGでIEがFirefoxやChromeと表示が異なるのはIEの「バグ」なのか?

ますはインラインSVGのテストケース。 以下、このテストケースを参照しながら記述していきます。

テストケースでは、IEでの表示サイズがおかしい(ように見える)、すなわち、FirefoxやChromeとは一致していない例は「赤丸」で示しています。

さて、「赤丸」となっている例には、共通する特徴が2つあることにお気づきでしょうか? (ただし、5-2~5-3の'slice'の例は除きます)

1つめは、svg要素に「高さ」が明示されていないときにIEでの表示が異なっている、という点です。

「でもCSSで'height:100%'って指定している例とかも赤いじゃんか」と思われるかもしれませんが、この場合、%での指定は固有の高さを与えるということにはなっていません。 FirefoxやChromeなどでテストケースの3-3と3-4を見比べてみて下さい。

SVGは「画像」ではありますが、言ってしまえば、単なる「座標」です。 PNGやJPGなどとは違って、あらかじめ幅や高さを持ってはいないので、CSSで幅や高さを明示してあげる必要があります。

では、適切な幅や高さが明示されていなかった場合にはどうなるのか? というと、SVG1.1の仕様ではフォールバックとしてSVGの「固有サイズ」(intrinsic size)を次のように規定しています。

他の言語の中に含められるようにするため、 SVG には何らかの固有の( intrinsic )大きさを表すプロパティの算出方法が指定される必要がある。 SVG 内容の ビューポート の固有の幅と高さは width, height 属性から決定されなければならない。 これらのいずれも指定されていない場合、値 '100%' と見なされなければならない。 注記: width, height 属性は CSS width, height プロパティと同じもの ではない 。 特に、百分率値は 固有の幅や高さを与えるものでも,内容ブロックの百分率を指示するものでもない。 それらはむしろ、ビューポートが一度確立された後,画像データに実際に覆われるビューポートの領域を指示する。

ちなみに、ここでいっているビューポートとはsvgの描画領域、すなわちsvg要素自身です。

そしてFirefoxやChromeでの表示は、適切な幅と高さが与えられなかった場合にこのフォールバックが働いて、こちらの手抜き(指定の不備)を補って、よしなに表示してくれたその結果なわけです。

では、IEはなぜそうならないのか?

2つの共通する特徴がある、と先に述べましたが、その2つめの特徴とは、svg要素の「高さ」が常に150pxであるということです。

赤丸の画像自体の高さ(直径)ではなく点線で示しているsvg要素の高さであることにご注意下さい。

この「高さ150px」は一体どこから来ているのでしょうか? いろいろ調べたところ、どうやら、SVGの仕様からではなく、CSS2.1の仕様を根拠としているようです。

'height'および'width'が'auto'の算出値を持ち、かつその要素が固有比を持つが固有の高さまたは幅を持たない場合、'width'の使用値はCSS 2.1では未定義である。しかし、その包含ブロックの幅が置換要素の幅に依存しない場合、'width'の使用値は通常フローでの非置換ブロックレベル要素に用いる拘束方程式より計算されることが示唆される。

そうでなければ、'width'が'auto'の算出値を持つ場合、その要素は固有幅を持ち、その結果その固有幅は'width'の使用値となる。

そうでなければ、上記の条件に一致せず、'width'が'auto'の算出値を持つ場合、'width'の使用値は300pxになる。デバイスにあわせて300pxが広すぎる場合、ユーザーエージェントは2:1の比率を持つ最大の矩形の幅を使用し、代わりにデバイスに収めるべきである。

このCSS2.1の仕様に沿って、IEでは適切な幅や高さが取れない場合には「300px(以上)*150px」で表示しているわけです。

ただし、

  • デバイスの幅が300pxに満たない場合には、2:1の比率を持つ最大の矩形の幅を使用しろってなっているのに、なぜ300px以上ある場合にも高さが150px固定となるのか
  • その一方で、テストケースの1-0の例を見ればおわかりの通り、そもそも幅が300pxではなく親要素の幅一杯まで広がっているのはなぜなのか

そのあたりの理由についてはよくわかりませんが、まぁそんなものかね、ということで先に進みます。 CSS3が出ている現在、過去の仕様との互換性をつきつめてもあまり意味はありませんw

いずれにせよ、IEの固有サイズ表示で高さが150pxに固定されるのは、「バグ」というわけではなく、単にSVG1.1の仕様をIEがまだ実装していないだけであり、実は、IEはCSS2.1の仕様に沿ってフォールバックを適切に(?)働かせている、ということになります。

IEもIEで、こちらの手抜き(指定の不備)を補って、よしなに表示してくれているわけですw

IEにも対応したレスポンシブSVGの作り方(JavaScript適用版)

@_tanshioさんのおっしゃっているレスポンシブとは、昔で言うところの「リキッドデザイン」的なことだと思いますが、その記事の主題であった「幅が可変(不定)な親要素にあわせて、svgの持つアスペクト比を保ちながらインラインsvgを親要素の幅一杯に表示(拡大・縮小)する」にはどうすればいいのでしょうか。

「幅」は'max-width:100%;'の指定でまだどうにかなるとして、適切な「高さ」がCSSで明示できない以上、これはCSSだけではいかんともしようがありません。

ただし、幸い、IEで固有サイズ表示がされている場合には、「必ず高さが150pxになる」ということはわかっています。 そこで、JavaScriptを使って、IEで固有サイズ表示がされている場合にはアスペクト比から適切な幅と高さを再指定してあげるような修正スクリプトを作ってみました。 ついでに、5-2と5-3の例にも対応してあります。


/**
 * fixSvgIntrinsicSizing
 * IEのインラインSVGの固有サイズ表示をSVG1.1仕様に修正
 * おまけで、preserveAspectRatioが'slice'だった場合に'overflow:hidden'を適用
 * 注意:svg要素にはviewBox属性を必須としています
 * なお、特定のsvg要素への適用を避けたい場合には、そのsvg要素にclass="noFixSvgIntrinsicSizing"を付与してください
 * Fix inline SVG intrinsic sizing in IE.
 * Plus, add 'overflow:hidden' when preserveAspectRatio has 'slice' value.
 * NOTE: 'viewBox' attribute is required in svg element.
 * To prevent applying to the specific svg element, add class="noFixSvgIntrinsicSizing" to that svg element.
 * Copyright (c) 2014-2016 Kazz
 * http://asamuzak.jp
 * Dual licensed under MIT or GPL
 * http://asamuzak.jp/license
 */

(function(_win, _doc) {
  "use strict";
  if(_doc.documentMode) {
    var fixSvgIntrinsicSizing = function() {
      function getAspect(o) {
        return (o = o.split(" ")) && o[3] / o[2];
      }
      var x = _doc.querySelectorAll("svg[viewBox]");
      if(x) {
        for(var y, z, a, b, i = 0, l = x.length; i < l; i++) {
          y = x[i];
          if(!/noFixSvgIntrinsicSizing/.test(y.className.baseVal)) {
            y.hasAttribute("preserveAspectRatio") &&
              /slice/.test(y.getAttribute("preserveAspectRatio")) &&
              (y.style.overflow = "hidden");
            a = _win.getComputedStyle(y, "").width;
            b = _win.getComputedStyle(y, "").height;
            y.style.width = "";
            y.style.height = "";
            z = _win.getComputedStyle(y, "").height;
            if(z !== "150px") {
              y.style.width = a;
              y.style.height = b;
            }
            else {
              z = _win.getComputedStyle(y, "").width;
              a = /([0-9\.]+)px/.exec(z)[1] * 1;
              b = getAspect(y.getAttribute("viewBox"));
              a * b > _doc.documentElement.offsetHeight && (
                y.style.height = (a * b) + "px",
                z = _win.getComputedStyle(y, "").width,
                a = /([0-9\.]+)px/.exec(z)[1] * 1
              );
              y.style.width = z;
              y.style.height = (a * b) + "px";
            }
          }
        }
      }
    };
    _doc.addEventListener("DOMContentLoaded", fixSvgIntrinsicSizing, false);
    _win.addEventListener("resize", fixSvgIntrinsicSizing, false);
  }
})(window, document);

そして、テストケースの修正スクリプト適用後のサンプルはこちら

スクリプトを使うにあたっての注意事項は2点。

  • svg要素にはviewBox属性を付与しておくことが必要です。 viewBox属性がないとこのスクリプトは適用されません(アスペクト比が取れないので)。
  • preserveAspectRatio="none"で、かつ、heightを150pxに指定していた場合、スクリプトによってアスペクト比が有効に戻されてしまいます。 このようなケースなど、もし特定のsvg要素への適用を避けたい場合には、そのsvg要素にclass="noFixSvgIntrinsicSizing"を付与してください。

svgの入れ子があった場合にどうなるかなどテストし切れていないケースもありますが、でもそれなりに対応できているのではないかと思います。

ということで、このスクリプトを使っておけば、Michael Schenker、まぁ、いけるし、ええんか~?

お後がよろしいようで…m(_ _)m (でも今のところ翌日のカレンダーは空いているようですが…orz)

比較参照できるよう、img要素でSVGを使った場合のテストも追加しました。

"♪Vector Vector, please. Oh, the mess I'm in (c) UFO"へのTwitter上でのコメントやRT

34件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6

ツイート 7

ツイート 8

ツイート 10

ツイート 11

ツイート 12

ツイート 13

ツイート 14

ツイート 15

ツイート 16

ツイート 17

ツイート 18

ツイート 19

ツイート 20

ツイート 21

ツイート 22

ツイート 23

ツイート 24

ツイート 25

ツイート 26

ツイート 27

ツイート 28

ツイート 29

ツイート 30

ツイート 31

ツイート 32

ツイート 34