Opera の drawImage(SVGSvgElement) の実装には改善の余地があるみたい

今日もHTML5::Canvas ネタです。

先日、id:edvakf さんにヒントをいただいたので、drwaImage(SVGSvgElement) で Text API の実装を試してみました。

drwaImage(SVGSvgElement,...) では実装できないのでしょうか?
もしかして Opera だけ?
http://tc.labs.opera.com/html/canvas/svg/

テキストの表示はうまくいくのですが、dropShadow を実装しようとしたところ問題が発覚。

上が ガウスフィルター付きのSVGの描画で、
下が drawImage(SVGSvgElement) を実行したときの描画結果です。

// SVG + ガウスフィルター のコードは、こんな感じ
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="dropshadow">
      <feGaussianBlur in="SourceGraphic" result="blur"
        stdDeviation="1.2" />
      <feOffset in="blur" result="offsetBlur" dx="4" dy="3"/>
      <feMerge>
        <feMergeNode in="offsetBlur"/>
      </feMerge>
    </filter>
  </defs>
  <text fill="pink" font-size="20" x="40" y="40" filter="url(#dropshadow)">abcdef</text>
</svg>


drawImage を通すと画質がかなり劣化してしまうようです。
Opera9.52 〜 10α で確認しました。

もったいないから書き残す

チェックインしないつもりなので、ここにコードを書き残します。
# 最適化前なので、ちょっと不要なコードがあるけど気にしない。

// for Opera9.5 - 10.0 ( drawImage(SVGSvgElement) method )
if (uuCanvas.extendTextAPI && uu.ua.opera
                           && (uu.ua.ver >= 9.5 && uu.ua.ver <= 10)) {

  uu.mix(CanvasRenderingContext2D.prototype, {
    textAlign: "start",
    textBaseling: "top",
    fillText: function(text, x, y, maxWidth, wire) {
      function svge(name) {
        return uudoc.createElementNS("http://www.w3.org/2000/svg", name);
      }
/*
  <defs>
    <filter id="dropshadow">
      <feGaussianBlur in="SourceGraphic" result="blur"
        stdDeviation="..." />
      <feOffset in="blur" result="offsetBlur" dx="..." dy="..."/>
    </filter>
  </defs>
 */
      function filter(svg, sx, sy, sb, sc) {
        var e = [];
        svg.appendChild(e[0] = svge("defs"));
          e[0].appendChild(e[1] = svge("filter"));
            e[1].appendChild(e[2] = svge("feGaussianBlur"));
            e[1].appendChild(e[3] = svge("feOffset"));
//          e[1].appendChild(e[4] = svge("feMerge"));
//            e[4].appendChild(e[5] = svge("feMergeNode"));
        e[1].setAttribute("id", "dropshadow");
        e[2].setAttribute("in", "SourceGraphic");
        e[2].setAttribute("result", "blur");
        e[2].setAttribute("stdDeviation", sb / 2);
        e[3].setAttribute("in", "blur");
        e[3].setAttribute("result", "offsetBlur");
        e[3].setAttribute("dx", sx);
        e[3].setAttribute("dy", sy);
//      e[5].setAttribute("in", "offsetBlur");
      }
      var svg = svge("svg"),
          txt = svge("text"), txt2, 
          align = TEXT_ALIGNS[this[TEXT_ALIGN]] || 1,
          metric = getTextMetric(text, this[FONT]),
          sc = _colorCache[this[SHADOW_COLOR]] || _addColorCache(this[SHADOW_COLOR]),
          offX = 0, offY = 0, blurSpace = 100;

      if (align !== 1) { // 1: "start" or "left"
        offX = (align === 2) ? metric.w / 2 : metric.w; // 2 = "center"
      }
      offY = (metric.h + metric.h / 2) / 2; // emulate textBaseLine="top"

      svg.setAttribute("width",  metric.w + blurSpace);
      svg.setAttribute("height", metric.h + blurSpace);

      if (sc[1] && (this[SHADOW_OFFSET_X] || this[SHADOW_OFFSET_Y])) {
        filter(svg, this[SHADOW_OFFSET_X], this[SHADOW_OFFSET_Y],
               this[SHADOW_BLUR], sc);

        txt2 = svge("text");
        txt2.setAttribute("x", 0 + blurSpace / 2);
        txt2.setAttribute("y", offY + blurSpace / 2);
        txt2.setAttribute("fill", sc[0]);
        txt2.setAttribute("opacity", 1);
        txt2.setAttribute("style", "font:" + this[FONT]);
        txt2.setAttribute("filter", "url(#dropshadow)");

        svg.appendChild(txt2);
        txt2.appendChild(uudoc.createTextNode(text));
      }

      txt.setAttribute("x", 0 + blurSpace / 2);
      txt.setAttribute("y", offY + blurSpace / 2);
      txt.setAttribute("fill", this[FILL_STYLE]);
      txt.setAttribute("style", "font:" + this[FONT]);

      svg.appendChild(txt);
      txt.appendChild(uudoc.createTextNode(text));

      uudoc.body.appendChild(svg);
      this.drawImage(svg, x - offX - blurSpace / 2, y - blurSpace / 2);
      uudoc.body.removeChild(svg);
    },

    strokeText: function(text, x, y, maxWidth) {
      this.fillText(text, x, y, maxWidth, 1);
    },

    measureText: function(text) {
      var metric = getTextMetric(text, this[FONT]);
      return new TextMetrics(metric.w, metric.h);
    }
  });
}

おまけ

実は Firefox3 でも strokeText をサポートできるのですが、リリースするバージョンでは、機能をつぶします(fillTextと同じ結果にします)。
理由は、Geckoに実装されているAPI(mozPathText)を呼び出すと、カレントパスを変更してしまうので、他のブラウザと描画結果が異なるなど、多くの混乱が起きそうだからです。


if (uuCanvas.extendTextAPI) {
  if (uu.ua.gecko && uu.ua.ver === 3) { // for Firefox3.0
    uu.mix(CanvasRenderingContext2D.prototype, {
      textAlign: "start",
      textBaseling: "top",
      fillText: function(text, x, y, maxWidth, wire) {
        var align = TEXT_ALIGNS[this[TEXT_ALIGN]] || 1,
            metric = getTextMetric(text, this[FONT]),
            offX = 0, offY = 0;

        if (align !== 1) { // 1: "start" or "left"
          offX = (align === 2) ? metric.w / 2 : metric.w; // 2 = "center"
        }
        offY = (metric.h + metric.h / 2) / 2; // emulate textBaseLine="top"

        this.save();
        this.mozTextStyle = this.font;
        this.translate(x - offX, y + offY);
if (0) { // strokeText をサポートする場合は、if (1) にする
        if (wire) {
          this.beginPath(); // reset path
          this.mozPathText(text);
          this.closePath();
          this.stroke();
        } else {
          this.mozDrawText(text);
          // http://d.hatena.ne.jp/uupaa/20090506/1241572019
          this.fillRect(0,0,0,0); // force redraw(Firefox3 TextAPI doesn't redraw)
        }
} else {
        if (wire) {
          this[FILL_STYLE] = this[STROKE_STYLE];
        }

        this.mozDrawText(text);
        // http://d.hatena.ne.jp/uupaa/20090506/1241572019
        this.fillRect(0,0,0,0); // force redraw(Firefox3 TextAPI doesn't redraw)
}

        this.restore();
      },
      strokeText: function(text, x, y, maxWidth) {
        this.fillText(text, x, y, maxWidth, 1);
      },

      measureText: function(text) {
  //    return new TextMetrics(this.mozMeasureText(text), 0);
        var metric = getTextMetric(text, this[FONT]);
        return new TextMetrics(metric.w, metric.h);
      }
    });

反省会

  • TextShadow は、CSS::text-shadow で描画する実装が既にあり、品質もCSS式のほうが良いので、drawImage(SVGSvgElement) 方式はお蔵入りする予定。
    • と思ったけどどうしようか考え中。Shadow blurVMLやSilverlight1,2のように、なんちゃって実装(透明度をいじってずらして重ね打ち)すればいい話なので、やっぱり残すべきなのかな? う〜む。
  • 昨日見つけた Gecko の不具合や、今日見つけた Opera の問題はどちらも独自実装の部分なので、バグレポート出し辛いなぁ…
  • SVG で DropShadow は http://dev.opera.com/articles/view/svg-evolution-3-applying-polish/?page=2 とかが参考になります。
  • コードを書いては、ちぎっては投げ、ちぎっては捨て。
    • やりきれない。