風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

SVGでテキストの縦位置(baseline)を調整する方法を調べてみた

SVGで、テキストの縦位置(baseline)を揃える処理を書こうとしてはまったので、覚え書き。

経緯

コピィ・ライターを作成していてふと、
「あれ? 基本的にテキストを配置してバナーを作るものだし、SVGと相性良いんじゃ? 設定ファイルを JSON で書き出しているところをちょっといじれば、SVG で書き出すこともできるようになるかも?」
と思い立ったので、処理を追加してみたところ、テキストの baseline を揃えるところで苦労したというもの。
そもそも、SVGをいじるのはこれが初めてなので、誤解をしているところはあるかも……変なところがあればご指摘願いたい。

どこではまったか? 「え……またIEかよっ!」

HTML5 Canvas の場合、textBaseline プロパティを指定すればテキストの baseline が指定できていた。
同じようなものが SVG でも無いか探してみると、text 要素等で指定できる dominant-baseline プロパティがこれに相当するようだ。

HTML5 Canvas と SVG のテキストの baseline の対応(今回対象としたもののみ)
baseline(基底線) Canvas
(textBaselineプロパティ)
SVG
(dominant-baselineプロパティ)
The top of the em square top text-before-edge
The middle of the em square middle central
The bottom of the em square bottom text-after-edge

em square=文字の構造的な大きさを表す四角形、EM box・em quad 等とも。

■参考

textBaseline プロパティ - Canvasリファレンス - HTML5.JP
svg要素の基本的な使い方まとめ


それで、実際やってみたところ……

のように、IE では効いていないように思われた。


それで調べてみると、確かにサポートされていないようだ。

IE9 Mode, IE10 Mode, IE11 Mode, and EdgeHTML Mode (All
Versions)

The dominant-baseline attribute is not supported.

[MS-SVG]: [SVG11] Section 10.9.2, Baseline alignment properties

ではどうするか?

とりあえず、IE でなんとかする方法を探してみたところ、

The specification defines central like that:


central


This identifies a computed baseline
that is at the center of the EM box.

We can take an EM box of known font size and measure its bounding box to compute the center.

internet explorer 9 - How to center SVG text vertically in IE9 - Stack Overflow

とあり、具体的なソースコードが書かれている。


どうやら、SVG中の script 要素にて、EM box の座標と大きさを元に central 相当の位置を決めて、transform 属性にて位置を調整する、ということらしい。

  • SVG 中の text 要素に指定した座標を (Tx, Ty)
  • 実際に描画された当該 text 要素の EM box(外枠) の左上の座標を (Rx, Ry)、高さを Rh


とすると、

text-before-edge 相当位置 Ry + (Ty - Ry)
central 相当位置 Ry + ( (Ty - Ry) - (Rh / 2) )
text-after-edge 相当位置 Ry + ( (Ty - Ry) - Rh )

になると思われる。


transform 属性には、

translate(<tx> [<ty>])
tx, ty による 並進 を指定する。
<ty> が与えられていない場合、 0 とみなされる。

座標系, 変換, 単位 – SVG 1.1 (第2版)

のような変換定義を指定してやればテキストの移動が可能なので、例えば central の場合には
tx = 0, ty = (Ty - Ry) - (Rh / 2)
を指定してやればよい。
tx, tyは、EM box左上端からの相対位置なので注意。


コピィ・ライターでは、具体的には、以下のようなスクリプトを SVG 内に挿入した。

var useragent = window.navigator.userAgent.toLowerCase();
if (useragent.indexOf('msie') < 0 && useragent.indexOf('trident') < 0) {
    return; // IE 以外は何もしない
}
window.onload = function() {
    var elm_text_list = document.getElementsByTagName('text');
    for (var ci=0, len = elm_text_list.length; ci < len; ci++) {
        var elm_text = elm_text_list[ci]
        ,   text_y = parseInt(elm_text.getAttribute('y'))
        ,   rect = elm_text.getBBox()
        ,   rect_y = rect.y
        ,   rect_height = rect.height
        ,   offset_y = (text_y - rect_y) - (rect_height / 2); // central の場合
        
        elm_text.setAttribute('transform', 'translate(0, ' + offset_y + ')');
    }
};


なお、IE 以外の場合には、別途 SVG の text 要素に対して dominant-baseline プロパティを設定してあるため、スクリプトでは処理を行わない。

結果

実際に上記手法を適用して作成したSVG画像が、Chrome と IE でどう見えるかを示す。
赤線は基線(SVGのテキスト要素で指定したY座標)、テキストの周囲の四角はEM box(スクリプトで描写)

Google Chrome (バージョン 43.0.2357.132 m)


Windows 7 上で確認した限りでは、Firefox や Opera の最新版でもほぼ同様に見えた。

IE11

注意点

  • SVG ファイルを HTML中に貼り付ける際、IMG要素 を使用すると、SVG内のスクリプトも実行されないため、上記の小細工が適用されない。
    これはCSSのbackground-image等で指定する場合でも同様。
  • OBJECT要素や IFRAME要素で指定してやれば、意図通りに表示される。
    ただし、IFRAMEだと扱いが比較的面倒であるため(CSSで拡大縮小しづらい等)、OBJECT要素を使用するのがベター。

HTML5・A要素(リンク)のdownload属性に関する覚え書き

これもコピィ・ライター作成時に、

  • 動的に生成した画像をボタンをクリックしてダウンロード

する機能を実現する過程で、HTML5・A要素(リンク)の download 属性について調べたことに関する覚え書き。



HTML5・A要素(リンク)の download 属性とは


download HTML5


この属性は、ユーザがリンクをクリックするとリソースをローカルファイルとして保存することを促されるように、リソースをダウンロードするために使用されるハイパーリンクであることをページ作者が意図して記述します。属性に値が指定された場合、ユーザがリンクをクリックしたときに開く保存プロンプトの、デフォルトのファイル名として解釈します (もちろん、ユーザは実際にファイルを保存する前にファイル名を変更できます)。使用可能な値に制限はありません (ただし / および \ はアンダースコアに変換して、特定のパスヒントを防ぎます) が、多くのファイルシステムには、ファイル名に使用できない文字があることを考慮する必要があります。ブラウザがファイル名を調整するかもしれません。

補足:

  • この属性は、ユーザが簡単に JavaScript を使用するプログラムで生成されたコンテンツ (例えばオンラインのお絵かき Web アプリを使用して描いた画像) をダウンロードするため、blob: URL および data: URL とともに使用できます。
  • この属性で指定したものと異なるファイル名を Content-Disposition: HTTP ヘッダで与えている場合は、この属性より HTTP ヘッダが優先します。
  • この属性を指定するとともに Content-Disposition:inline を指定している場合、Firefox はファイル名と同様に Content-Disposition を優先しますが、Chrome は download 属性を優先します。
  • Firefox 20 では、この属性は同一生成元のリソースへのリンクにのみ受け入れられます。

a 要素 - HTML | MDN

とりあえず、Can I use... Support tables for HTML5, CSS3, etcで調べてみると、最新のFirefox・Chrome・Operaはサポートしている模様。
またIEは無いのか……と思ったが、代替手段(window.navigator.msSaveOrOpenBlob())でなんとかなりそうではあった。
Safari? えっと、知らない子ですね……。

実験とブラウザ毎の結果

  • [A] 画像ファイルを Data URL に変換したもの
  • [B] 画像ファイル(サイト内)へのリンク
  • [C] 画像ファイル(外部サイト)へのリンク

三種類の A(リンク)要素を用意し、それぞれに download 属性でファイル名を指定した場合のテストを行った。
IE用には別途、スクリプトで window.navigator.msSaveOrOpenBlob() を使った細工をしてある。

それぞれのリンクをクリック(タップ)した結果、以下のようになった。

Google Chrome
43.0.2357.134 m
Firefox
39.0
Opera
30.0.1835.125
IE11 Chrome for Android
43.0.2357.93
[A]
[B] ×
[C] × ×
  • ○:download属性で指定したファイル名でダウンロード
  • △:download属性は無視され、href属性を元にしたファイル名でダウンロード
  • ▲:download属性で指定したファイル名でダウンロード
    IE用に、リンクに onclick トリガを設定し、window.navigator.msSaveOrOpenBlob() を使った細工を入れている
  • ※:別窓(タブ)で画像が開く
    IE用に、リンクに onclick トリガを設定し、XMLHttpRequest で画像を取得させ、エラーが発生したら別窓(タブ)で開く細工を入れている
  • ×:hrefで指定された先にページ遷移

外部サイト上のファイルの場合の動作はセキュリティがらみの制限なのだろうと推測されるが、Chrome for Android でサイト内ファイルへのリンクまでページ遷移してしまうのは謎。なぜ PC 版と動作を変える必要があったのか?

関連する処理など

canvas 要素の Data URL への変換

参考:toDataURL() メソッド - Canvasリファレンス - HTML5.JP

var dataURL = canvas.toDataURL(type); // canvas は HTML5 Canvas の DOM要素、typeは画像の種別('image/png', 'image/jpeg'等)
データの Blob オブジェクトへの変換

参考:Blob - Web API インターフェイス | MDN

var blobObject = new Blob([data], {'type' : mimeType}); // data は元データ、mimeType は元データの MIMEタイプ('image/svg+xml'等)
Data URL の Blob オブジェクトへの変換

参考:Canvas に描いた画像を png などの形式の Blob に変換する方法: Tender Surrender

function make_blob_from_dataurl(dataurl) {
    if (!dataurl.match(/^data:(.*?);base64,(.*)$/)) {
        return null;
    }
    var type = RegExp.$1, base64 = RegExp.$2;
    
    var bin = atob(base64), bin_length = bin.length;
    var buffer = new Uint8Array(bin_length);
    for (var ci=0; ci < bin_length; ci++) {
        buffer[ci] = bin.charCodeAt(ci);
    }  
    var blobObject = new Blob([buffer.buffer], {type: type});
    
    return blobObject;
}
ファイルを Blob オブジェクトとして取得

参考:バイナリデータの送信と受信 - XMLHttpRequest | MDN

var xhrObject = new XMLHttpRequest();
xhrObject.open('GET', url, true);
xhrObject.responseType = 'blob';
xhrObject.onload = function(event) {
    var blobObject = xhrObject.response;
    // ◆ 以下、blobObject を用いた処理を記述
};
xhrObject.onerror = function(event) {
    //※(許可されていない)外部サイトのファイルを取得しようとすると以下のようなエラーが発生(IEの例)
    //  SEC7118: (取得しようとしたURL) の XMLHttpRequest には Cross Origin Resource Sharing (CORS) が必要です。
    //  ファイル: (元ファイル)
    //  SEC7120: 元の http://(元ドメイン) が Access-Control-Allow-Origin ヘッダーに見つかりません。
    //  ファイル: (元ファイル)
    //  SCRIPT7002: XMLHttpRequest: ネットワーク エラー 0x80070005, アクセスが拒否されました。
    
    window.open(url);   // 次善の策として、例えばポップアップで開く
};
xhrObject.send();
Blob オブジェクトの Blob URL への変換

参考:window.URL.createObjectURL - Web API インターフェイス | MDN

var blobURL = (window.URL || window.webkitURL).createObjectURL(blobObject);
Blob オブジェクトをファイルとして保存したり開いたりするイベントの発生(IE10+)

参考:msSaveOrOpenBlob method (Internet Explorer)

window.navigator.msSaveOrOpenBlob(blobObject, filename);

その他気付いたことなど

  • A要素(リンク)の場合、jQuery によってクリックイベントを発生( .click() や .trigger('click'))させても、ダウンロードやページ遷移は行われない。
    これらを発生させたい場合には、MouseEvent を作成するか、DOM の .click() をコールする。