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>