NPOI で xlsx を出力すると、なぜか下線が引かれてしまう

NPOI 2.2.1 で 2007 形式の Excel ファイル (.xlsx) を出力した際、なぜか文字にすべて下線が引かれてしまうため調べてみた結果、以下のような問題を発見しました。(PR 済み)

  • XSSFFont の下線情報の設定処理がおかしい
  • FontUnderline.NONE が内部値がおかしい

この合わせ技によりバグが発生します。

まず XSSFFont.cs の該当部実装がこちら。

internal void SetUnderline(FontUnderlineType underline)
{
    if (underline == FontUnderlineType.None && _ctFont.sizeOfUArray() > 0)
    {
        _ctFont.SetUArray(null);
    }
    else
    {
        CT_UnderlineProperty ctUnderline = _ctFont.sizeOfUArray() == 0 ? _ctFont.AddNewU() : _ctFont.GetUArray(0);
        ST_UnderlineValues val = (ST_UnderlineValues)FontUnderline.ValueOf(underline).Value;
        ctUnderline.val = val;
    }
}

新しい値が None で、かつ内部で既に下線の値を保持している場合、内部に保持している下線情報をクリアします。

そして、そうではない場合は既に内部に下線の値を保持している場合はその情報を、保持していない場合は新しい下線情報を作成した上で、指定された値を保持します。

この情報は出力結果となる xslx 内にある styles.xml に u 要素の出力に反映され、値が設定されていない場合 (_ctFont.sizeOfUArray() == 0 の時) は u 要素が出力されず、値が存在する場合はその値が出力されます。

出力される値は FontUnderlineType.cs にある None の値 となりますが、こうなっています。

public static readonly FontUnderline NONE = new FontUnderline(5);

これを踏まえた上で、下線を設定しない状況で明示的に下線なしの指定をしてみます。

var font = ((XSSFWorkbook)workbook).CreateFont();
font.Underline = FontUnderlineType.None;

この状況の場合、font の内部に保持している下線の情報が存在しており、ここに None の値がセットされていることになります。このため、u 要素が出力されます。

これで出力すると styles.xml に <u val="5"/> が出力されますが、Excel 上で見ると下線が引かれてしまうのです。

そして……。

var font = ((XSSFWorkbook)workbook).CreateFont();
font.Underline = FontUnderlineType.None;
font.Underline = FontUnderlineType.None;

SetUnderline() のコードを見た上であれば、これにより何が起きるかは想像できると思います。

そう、この場合は 2 回目の値の設定により、内部の下線情報がクリアされます。

このため、styles.xml に u 要素が出力されません。

つまり、下線なしにしたい場合は None を「設定しない」か、偶数回設定したらいいことになります。そんな馬鹿な!

さて、ここで FontUnderline.cs を少しだけいじってみます。

まず NONE の値を 5 から 0 に。

public static readonly FontUnderline NONE = new FontUnderline(0);

次に、静的コンストラクタの処理をほんのちょっとだけ変更。

static FontUnderline()
{
    if (_table == null)
    {
        _table = new FontUnderline[5];
        _table[0] = FontUnderline.NONE;
        _table[1] = FontUnderline.SINGLE;
        _table[2] = FontUnderline.DOUBLE;
        _table[3] = FontUnderline.SINGLE_ACCOUNTING;
        _table[4] = FontUnderline.DOUBLE_ACCOUNTING;
    }
}

(_table = new FontUnderline[6]; を [5] に、_table[5] だった NONE を [0] に移動しています)

この状態で、先と同じように一度だけ指定して出力してみます。

var font = ((XSSFWorkbook)workbook).CreateFont();
font.Underline = FontUnderlineType.None;

この場合、u 要素は <u val="none"/> と出力されます。あれ?(笑)

Office Open XML の仕様である ECMA-376 の Part 4 にこの辺りの定義が書かれていますが、スプレッドシートのフォントスタイルにおける u 要素の値は以下のようになっています。

sml_ST_UnderlineValues = 
  string "single" 
  | string "double" 
  | string "singleAccounting" 
  | string "doubleAccounting" 
  | string "none" 

そう、"5" はおかしくて "none" が正しいのです。そして、この場合 (当然) 下線は引かれません。(そもそも 0 にすると適切に "none" を出すって……)

ということで、この点を 0 にすると (とりあえず 2007 形式では) 適切な u 要素が出力されるようになります。

しかし、そもそも <u val="none"/> って、出す必要ないよね? という話があります。指定しなければ下線引かれないのですし。なのでこんな感じでいいのでは。

internal void SetUnderline(FontUnderlineType underline)
{
    if (underline == FontUnderlineType.None)
    {
        _ctFont.SetUArray(null);
    }
    else
    {
        CT_UnderlineProperty ctUnderline = _ctFont.sizeOfUArray() == 0 ? _ctFont.AddNewU() : _ctFont.GetUArray(0);
        ST_UnderlineValues val = (ST_UnderlineValues)FontUnderline.ValueOf(underline).Value;
        ctUnderline.val = val;
    }
}

(if の条件を None の場合のみに変更)

……ということで NPOI にこんな感じで PR だしたものの、POI だとどうなっているのだろうって確認してみたのですが、詳しく追いかけてないけど、XSSFFont.javaFontUnderline.java もまったく同じような……。