読者です 読者をやめる 読者になる 読者になる

WebKitの「下線がルビで分離される」バグを修正してみた

縦書きテキストレイアウトに関する調査支援者の公募にあるWebKitのバグを治すと10万円が貰える、というのに釣られてバグの修正に挑戦してみました。

挑戦してみたのは下線でルビが分離されるというやつです。コードのクォリティはアレな感じですが、とりあえず下記のような感じで動くようになりました。ちなみに下線以外の上線や取り消し線も同じ場所で表示してるので一緒に治ります。

Index: Source/WebCore/rendering/InlineTextBox.cpp
===================================================================
--- Source/WebCore/rendering/InlineTextBox.cpp	(revision 140537)
+++ Source/WebCore/rendering/InlineTextBox.cpp	(working copy)
@@ -1010,6 +1010,50 @@
         setClip = true;
     }
 
+    RenderBlock* containingBlock = renderer()->containingBlock();
+    if (containingBlock->isRubyBase() && containingBlock->parent() && containingBlock->parent()->isRubyRun()) {
+        RenderRubyRun* rubyRun = toRenderRubyRun(containingBlock->parent());
+
+        int leftOverhang = rubyRun->marginLeft();
+        int rightOverhang = rubyRun->marginRight();
+
+        RenderObject* prevObj = renderer();
+        while (prevObj != containingBlock) {
+            if (prevObj->previousSibling()) {
+                break;
+            }
+            prevObj = prevObj->parent();
+        }
+
+        RenderObject* nextObj = renderer();
+        while (nextObj != containingBlock) {
+            if (nextObj->nextSibling()) {
+                break;
+            }
+            nextObj = nextObj->parent();
+        }
+
+        bool hasNotLeft;
+        bool hasNotRight;
+        if (isLeftToRightDirection()) {
+            hasNotLeft = (prevObj == containingBlock);
+            hasNotRight = (nextObj == containingBlock);
+        } else {
+            hasNotLeft = (nextObj == containingBlock);
+            hasNotRight = (prevObj == containingBlock);
+        }
+
+        if (hasNotLeft && hasNotRight) {
+            width = rubyRun->logicalWidth() + leftOverhang + rightOverhang;
+            localOrigin.setX(boxOrigin.x() - left() - leftOverhang);
+        } else if (hasNotLeft) {
+            width += left() + leftOverhang;
+            localOrigin.setX(boxOrigin.x() - left() - leftOverhang);
+        } else if (hasNotRight) {
+            width = rubyRun->logicalWidth() - left() + rightOverhang;
+        }
+    }
+
     ColorSpace colorSpace = renderer()->style()->colorSpace();
     bool setShadow = false;

こんな感じでrubyの中でも途切れずに下線が表示されます。

f:id:mdgw:20130124181455p:plain

以下はこの修正の簡単な説明です。

HTMLで下記のようなタグがあった場合、

<ruby>
  漢字
  <rt>ふりがな</rt>
</ruby>

WebKitの内部では下記のような階層のデータ構造になるようです。

RenderRuby(<ruby>)
    RenderRubyRun
        RenderRubyText(<rt>)
            RenderText --> InlineTextBox(ふりがな)
            ...
        RenderRubyBase
            RenderText --> InlineTextBox(漢字)
            ...
    ...

問題となっている下線はInlineTextBox.cppのpaintDecorationメソッドで描画されているのですが、現状は文字幅の分だけ描画するようになっています。そこで、InlineTextBoxがRubyBase及びRubyRunの子どもであった場合には、RubyRunの横幅を利用して文字の外側の空白部分にも広げて描画するようにしています。

またルビ文字は、左右の文字に少しはみ出して表示されるのですが、そのはみ出した部分はoverhangと呼ばれるようです。このoverhangは、RubyRunにマイナスのmarginとして設定されるようなので、その値を利用して描画位置と幅を微調整しています。

それ以外で面倒なのが、下記のようなHTMLの場合です。

<ruby>
  <u><u>
  <strike></strike>
  <rt>ながいふりがな</rt>
</ruby>

f:id:mdgw:20130124181002p:plain

この場合InlineTextBoxが複数存在するため、単純にRubyRunの幅を利用することができません。現在は、単一・左端・中間・右端という4パターンに分けて対応しているのですが、この部分は冗長だったり座標の計算が怪しげだったりします。また上記のHTMLでは、取り消し線が「字」の両側に伸びておらず、文字の分割位置が偏っているのですが、修正が大変そうだったので手をつけていません。この辺り色々と改善の余地がある気がしています。