Subscribed unsubscribe Subscribe Subscribe

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

JavaScript OLE2 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>