JavaScriptでExcelのドキュメントを生成してみた

時間がないのでまだPoCの段階だし、手短に。

MS ExcelとかWordとかいろんなマイクロソフト製品の文章ファイルのバイナリ形式は「OLE2 Compound Document」というファイルシステム的な構造を持ったコンテナがベースとなってまして、昨日今日で、これを作ることのできるライブラリを作った、とそういう話です。

まあ、コンテナだけを作ってても何もうれしくないので、Excelの文章も吐かせるデモもつくってみました。「JavaScriptで音を鳴らしてみよう」でやんちゃしたように、今回もdataスキームに頼りきりなので肝心のIEでは動きません。

クリックするとExcelファイルが開きます (怖くないよ!)

デモプログラムのソースは以下のような感じ。まだ相当見苦しいけど、これをライブラリ化できるといいなあ、フューチャーワーク的に。

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <script type="text/javascript" src="ole2.js"></script>
  <script type="text/javascript">
with (OLE2CompoundDocument) {
var createDocument = function(doc) {
    var ob = new Builder(0x3e, 0x03, 512, 64, 4096);
    with (BinaryUtils) {
        var sbb = new BinaryBuilder();
        /* BOF */
        sbb.appendUInt16LE(0x0809);
        sbb.appendUInt16LE(16);
        sbb.appendUInt16LE(0x0600);
        sbb.appendUInt16LE(0x0005);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(2000);
        sbb.appendUInt32LE(0x40c1);
        sbb.appendUInt32LE(0x0106);

        /* CODEPAGE2 */
        sbb.appendUInt16LE(0x00e1);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1200);

        /* UNKNOWN (0xc1) */
        sbb.appendUInt16LE(0x00c1);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* UNKNOWN (0xe2) */
        sbb.appendUInt16LE(0x00e2);
        sbb.appendUInt16LE(0);

        /* WRITEACCESS */
        var user_name = 'test_user';
        sbb.appendUInt16LE(0x005c);
        sbb.appendUInt16LE(112);
        sbb.appendUInt16LE(user_name.length);
        sbb.appendUInt8(0);
        sbb.append(user_name);
        sbb.appendRepeatedBytes(0x20, 109 - user_name.length); 

        /* CODEPAGE */
        sbb.appendUInt16LE(0x0042); // e1?
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1200);

        /* DSF */
        sbb.appendUInt16LE(0x0161);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* UNKNOWN (0x1c0) */
        sbb.appendUInt16LE(0x01c0);
        sbb.appendUInt16LE(0);

        /* SHEETS (0x13d) */
        sbb.appendUInt16LE(0x013d);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* UNKNOWN (0x9c) */
        sbb.appendUInt16LE(0x009c);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0x0e);

        /* WINDOWPROTECT */
        sbb.appendUInt16LE(0x0019);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* PROTECT */
        sbb.appendUInt16LE(0x0012);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* PASSWORD */
        sbb.appendUInt16LE(0x0013);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* UNKNOWN (0x01af) */
        sbb.appendUInt16LE(0x01af);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* UNKNOWN (0x01bc) */
        sbb.appendUInt16LE(0x01bc);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* WINDOW1 */
        sbb.appendUInt16LE(0x003d);
        sbb.appendUInt16LE(18);
        sbb.appendUInt16LE(120);
        sbb.appendUInt16LE(30);
        sbb.appendUInt16LE(14955);
        sbb.appendUInt16LE(9345);
        sbb.appendUInt16LE(0x38);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(0x0258);

        /* BACKUP */
        sbb.appendUInt16LE(0x0040);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* HIDEOBJ */
        sbb.appendUInt16LE(0x008d);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* DATEMODE */
        sbb.appendUInt16LE(0x0022);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* PRECISION */
        sbb.appendUInt16LE(0x000e);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* UNKNOWN (0x01b7) */
        sbb.appendUInt16LE(0x01b7);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* BOOKBOOL */
        sbb.appendUInt16LE(0x00da);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(11 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(2);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(11 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(2);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(11 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(2);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(11 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(2);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(11 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(2);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FONT */
        {
            var font_name = "MS Pゴシック";
            sbb.appendUInt16LE(0x0031);
            sbb.appendUInt16LE(14 + font_name.length * 2 + 2);
            sbb.appendUInt16LE(6 * 20);
            sbb.appendUInt16LE(0);
            sbb.appendUInt16LE(0x7fff);
            sbb.appendUInt16LE(0x0190);
            sbb.appendUInt16LE(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(0);
            sbb.appendUInt8(0x80); //  SJIS
            sbb.appendUInt8(0);
            sbb.appendUInt8(font_name.length);
            sbb.appendUInt8(1); // no compression
            sbb.appendStringUCS2LE(font_name);
        }

        /* FORMAT */
        {
            var format_string = "\"\\\"#,##0;\"\\\"\\-#,##0";
            sbb.appendUInt16LE(0x041e);
            sbb.appendUInt16LE(format_string.length + 5);
            sbb.appendUInt16LE(5);
            sbb.appendUInt16LE(format_string.length);
            sbb.appendUInt8(0);
            sbb.append(format_string);
        }

        /* XF */
        sbb.appendUInt16LE(0x00e0);
        sbb.appendUInt16LE(20);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0xfff5);
        sbb.appendUInt8(0x20);
        sbb.appendUInt8(0x00);
        sbb.appendUInt8(0x00);
        sbb.appendUInt8(0x00);
        sbb.appendUInt32LE(0x00000000);
        sbb.appendUInt32LE(0x00000000);
        sbb.appendUInt16LE(0x20c0);

        /* STYLE */
        sbb.appendUInt16LE(0x0293);
        sbb.appendUInt16LE(4);
        sbb.appendUInt16LE(0x8000);
        sbb.appendUInt8(0);
        sbb.appendUInt8(0xff);

        /* USESELFS */
        sbb.appendUInt16LE(0x0160);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* SHEET */
        var sheet_name = "Sheet1";
        sbb.appendUInt16LE(0x0085);
        sbb.appendUInt16LE(sheet_name.length + 8);
        var co = sbb.chunks.length;
        sbb.appendUInt32LE(0); // offset to Sheet substream
        sbb.appendUInt8(0);
        sbb.appendUInt8(0);
        sbb.appendUInt8(sheet_name.length);
        sbb.appendUInt8(0);
        sbb.append(sheet_name);

        /* COUNTRY */
        sbb.appendUInt16LE(0x008c);
        sbb.appendUInt16LE(4);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(1);

        /* UNKNOWN (0x01c1) */
        sbb.appendUInt16LE(0x01c1);
        sbb.appendUInt16LE(8);
        sbb.appendUInt16LE(0x01c1);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0x6960);
        sbb.appendUInt16LE(1);

        /* SST */
        var str = "ほげげ";
        sbb.appendUInt16LE(0x00fc);
        sbb.appendUInt16LE(8 + 3 + str.length * 2);
        sbb.appendUInt32LE(1);
        sbb.appendUInt32LE(1);
        var sst_off = sbb.offset;
        {
            sbb.appendUInt16LE(str.length);
            sbb.appendUInt8(1);
            sbb.appendStringUCS2LE(str);
        }

        /* EOF */
        sbb.appendUInt16LE(0x0a);
        sbb.appendUInt16LE(0x00);

        // XXX: dirty
        sbb.chunks[co] = packUInt32LE(sbb.offset);
        /* BOF */
        sbb.appendUInt16LE(0x0809);
        sbb.appendUInt16LE(16);
        sbb.appendUInt16LE(0x0600);
        sbb.appendUInt16LE(0x0010);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(2000);
        sbb.appendUInt32LE(0x40c1);
        sbb.appendUInt32LE(0x0106);

        /* INDEX */
        sbb.appendUInt16LE(0x020b);
        sbb.appendUInt16LE(20);
        sbb.appendUInt32LE(0);
        sbb.appendUInt32LE(0);
        sbb.appendUInt32LE(1);
        var dcw_off = sbb.chunks.length;
        sbb.appendUInt32LE(0); // offset to DEFCOLWIDTH
        var dbc_off = [];
        dbc_off.push(sbb.chunks.length);
        sbb.appendUInt32LE(0); // offset to DBCELL

        /* CALCMODE */
        sbb.appendUInt16LE(0x000d);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* CALCCOUNT */
        sbb.appendUInt16LE(0x000c);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(100);

        /* REFMODE */
        sbb.appendUInt16LE(0x000f);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* ITERATION */
        sbb.appendUInt16LE(0x0011);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* DELTA */
        sbb.appendUInt16LE(0x0010);
        sbb.appendUInt16LE(8);
        sbb.appendIEEE754DoubleLE(0.001);

        /* SAVERECALC */
        sbb.appendUInt16LE(0x005f);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* PRINTHEADERS */
        sbb.appendUInt16LE(0x002a);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* PRINTGRIDLINES */
        sbb.appendUInt16LE(0x002b);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* GRIDSET */
        sbb.appendUInt16LE(0x0082);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(1);

        /* GUTS */
        sbb.appendUInt16LE(0x0080);
        sbb.appendUInt16LE(8);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);

        /* DEFAULTROWHEIGHT */
        sbb.appendUInt16LE(0x0225);
        sbb.appendUInt16LE(4);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0x010e);

        /* SHEETPR */
        sbb.appendUInt16LE(0x0081);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0x04c1);

        /* HEADER */
        sbb.appendUInt16LE(0x0014);
        sbb.appendUInt16LE(0);

        /* FOOTER */
        sbb.appendUInt16LE(0x0015);
        sbb.appendUInt16LE(0);

        /* HCENTER */
        sbb.appendUInt16LE(0x0083);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* VCENTER */
        sbb.appendUInt16LE(0x0084);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(0);

        /* PAGESETUP */
        sbb.appendUInt16LE(0x00a1);
        sbb.appendUInt16LE(34);
        sbb.appendUInt16LE(0); // paper size
        sbb.appendUInt16LE(0x010e); // scaling factor
        sbb.appendUInt16LE(1); // start page number
        sbb.appendUInt16LE(1); // fit worksheet width to this number of pages
        sbb.appendUInt16LE(1); // fit worksheet height to this number of pages
        sbb.appendUInt16LE(4); // flags
        sbb.appendUInt16LE(0); // horizontal resolution (dpi)
        sbb.appendUInt16LE(0); // vertical resolution (dpi)
        sbb.appendIEEE754DoubleLE(0.512); // header margin
        sbb.appendIEEE754DoubleLE(0.512); // footer margin
        sbb.appendUInt16LE(0); // number of copies to print

        sbb.chunks[dcw_off] = packUInt32LE(sbb.offset);
        /* DEFCOLWIDTH */
        sbb.appendUInt16LE(0x0055);
        sbb.appendUInt16LE(2);
        sbb.appendUInt16LE(8);

        /* DIMENSION */
        sbb.appendUInt16LE(0x0200);
        sbb.appendUInt16LE(14);
        sbb.appendUInt32LE(0);
        sbb.appendUInt32LE(1);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(0);

        var row_off = sbb.offset;
        /* ROW */
        sbb.appendUInt16LE(0x0208);
        sbb.appendUInt16LE(16);
        sbb.appendUInt16LE(0); // index of row
        sbb.appendUInt16LE(0); // index to the first column
        sbb.appendUInt16LE(1); // index to the last column
        sbb.appendUInt16LE(0x8000); // flags
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt32LE(0x010100); // flags

        /* LABELSST */
        sbb.appendUInt16LE(0x00fd);
        sbb.appendUInt16LE(10);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt32LE(0);

        var dboff = sbb.offset;
        sbb.chunks[dbc_off[0]] = packUInt32LE(dboff);
        /* DBCELL */
        sbb.appendUInt16LE(0x00d7);
        sbb.appendUInt16LE(6);
        sbb.appendUInt32LE(dboff - row_off); /* offset */
        sbb.appendUInt16LE(0);

        /* WINDOW2 */
        sbb.appendUInt16LE(0x023e);
        sbb.appendUInt16LE(18);
        sbb.appendUInt16LE(0x06b6);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0x40);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt32LE(0);

        /* SELECTION */
        sbb.appendUInt16LE(0x001d);
        sbb.appendUInt16LE(15);
        sbb.appendUInt8(3);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(1);
        sbb.appendUInt16LE(0);
        sbb.appendUInt16LE(0);
        sbb.appendUInt8(0);
        sbb.appendUInt8(0);

        /* PHONETICPR */
        sbb.appendUInt16LE(0x00ef);
        sbb.appendUInt16LE(6);
        sbb.appendUInt16LE(6);
        sbb.appendUInt16LE(0x35);
        sbb.appendUInt16LE(0);
 
        /* EOF */
        sbb.appendUInt16LE(0x0a);
        sbb.appendUInt16LE(0x00);

        var stream = ob.createStream(null, "Workbook", Math.max(4096, sbb.offset));
        ob.writeToStream(stream, sbb);
    }
    var bb = new BinaryBuilder();
    ob.build(bb);
    return bb;
}

window.onload = function() {
    var r = createDocument().result();
    location.href = "data:application/vnd.ms-excel," +escape(r);
};
}
  </script>
</head>
<body>
</body>
</html>