From 663270b7621d257528758c0b0b5561ea21ee5e3f Mon Sep 17 00:00:00 2001 From: SheetJS Date: Mon, 27 Mar 2017 17:35:15 -0400 Subject: [PATCH] XLS/XLSX/XLSB Interpret Chartsheets as Data Tables - chartsheets are now stored as sheets with "!type" set to "chart" - fixed Strict XML Workbook Relationships - stubbed support for dialog and macro sheets - removed legacy XLS `!range` field --- CHANGELOG.md | 4 + Makefile | 4 + README.md | 12 +- bits/21_ziputils.js | 12 + bits/30_ctype.js | 42 ++- bits/31_rels.js | 25 +- bits/34_extprops.js | 24 +- bits/38_xlstypes.js | 6 +- bits/39_xlsbiff.js | 40 ++- bits/47_styxml.js | 2 +- bits/49_theme.js | 2 +- bits/54_drawing.js | 18 ++ bits/66_wscommon.js | 5 +- bits/69_chartxml.js | 41 +++ bits/70_csheet.js | 47 +++ bits/74_xmlbin.js | 5 + bits/76_xls.js | 3 +- bits/77_parsetab.js | 2 +- bits/85_parsezip.js | 41 ++- bits/86_writezip.js | 2 +- docbits/54_shobject.md | 7 + docbits/{54_wbobject.md => 55_wbobject.md} | 0 docbits/81_writeopts.md | 4 +- test.js | 22 +- test_files | 2 +- tests.lst | 25 +- xlsx.flow.js | 317 ++++++++++++++++----- xlsx.js | 317 ++++++++++++++++----- 28 files changed, 796 insertions(+), 235 deletions(-) create mode 100644 bits/54_drawing.js create mode 100644 bits/69_chartxml.js create mode 100644 bits/70_csheet.js create mode 100644 docbits/54_shobject.md rename docbits/{54_wbobject.md => 55_wbobject.md} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5154a..6860b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ changes may not be included if they are not expected to break existing code. ## Unreleased +* XLS legacy `!range` field removed + +## 0.9.6 (2017-03-25) + * `sheet_to_json` now passes `null` values when `raw` is set to `true` * `sheet_to_json` treats `null` stub cells as values in conjunction with `raw` diff --git a/Makefile b/Makefile index 375d303..d46769e 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,10 @@ dist-deps: ## Copy dependencies for distribution .PHONY: aux aux: $(AUXTARGETS) +.PHONY: bytes +bytes: ## display minified and gzipped file sizes + for i in dist/xlsx.min.js dist/xlsx.{core,full}.min.js; do printj "%-30s %7d %10d" $$i $$(wc -c < $$i) $$(gzip --best --stdout $$i | wc -c); done + .PHONY: graph graph: formats.png ## Rebuild format conversion graph formats.png: formats.dot diff --git a/README.md b/README.md index fc5cd12..4831de5 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ with a unified JS representation, and ES3/ES5 browser compatibility back to IE6. + [Data Types](#data-types) + [Dates](#dates) * [Worksheet Object](#worksheet-object) + * [Chartsheet Object](#chartsheet-object) * [Workbook Object](#workbook-object) * [Document Features](#document-features) + [Formulae](#formulae) @@ -543,6 +544,13 @@ Special worksheet keys (accessible as `worksheet[key]`, each starting with `!`): will write all cells in the merge range if they exist, so be sure that only the first cell (upper-left) in the range is set. +### Chartsheet Object + +Chartsheets are represented as standard worksheets. They are distinguished with +the `!type` property set to `"chart"`. + +The underlying data and `!ref` refer to the cached data in the chartsheet. + ### Workbook Object `workbook.SheetNames` is an ordered list of the sheets in the workbook @@ -762,8 +770,8 @@ The exported `write` and `writeFile` functions accept an options argument: - `bookSST` is slower and more memory intensive, but has better compatibility with older versions of iOS Numbers -- The raw data is the only thing guaranteed to be saved. Formulae, formatting, - and other niceties may not be serialized (pending CSF standardization) +- The raw data is the only thing guaranteed to be saved. Features not described + in this README may not be serialized. - `cellDates` only applies to XLSX output and is not guaranteed to work with third-party readers. Excel itself does not usually write cells with type `d` so non-Excel tools may ignore the data or blow up in the presence of dates. diff --git a/bits/21_ziputils.js b/bits/21_ziputils.js index b44752f..13f3084 100644 --- a/bits/21_ziputils.js +++ b/bits/21_ziputils.js @@ -60,3 +60,15 @@ if (typeof exports !== 'undefined') { _fs = require('fs'); } } + +function resolve_path(path/*:string*/, base/*:string*/)/*:string*/ { + var result = base.split('/'); + if(base.slice(-1) != "/") result.pop(); // folder path + var target = path.split('/'); + while (target.length !== 0) { + var step = target.shift(); + if (step === '..') result.pop(); + else if (step !== '.') result.push(step); + } + return result.join('/'); +} diff --git a/bits/30_ctype.js b/bits/30_ctype.js index f020b9f..666fc92 100644 --- a/bits/30_ctype.js +++ b/bits/30_ctype.js @@ -10,17 +10,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Worksheet */ "application/vnd.ms-excel.binIndexWs": "TODO", /* Binary Index */ - /* Chartsheet */ - "application/vnd.ms-excel.chartsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": "TODO", - - /* Dialogsheet */ - "application/vnd.ms-excel.dialogsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": "TODO", - /* Macrosheet */ - "application/vnd.ms-excel.macrosheet": "TODO", - "application/vnd.ms-excel.macrosheet+xml": "TODO", "application/vnd.ms-excel.intlmacrosheet": "TODO", "application/vnd.ms-excel.binIndexMs": "TODO", /* Binary Index */ @@ -31,6 +21,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Custom Data Properties */ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", /* Comments */ "application/vnd.ms-excel.comments": "comments", @@ -110,6 +101,9 @@ var ct2type/*{[string]:string}*/ = ({ /* Themes */ "application/vnd.openxmlformats-officedocument.theme+xml": "themes", + /* Theme Override */ + "application/vnd.openxmlformats-officedocument.themeOverride+xml": "TODO", + /* Timeline */ "application/vnd.ms-excel.Timeline+xml": "TODO", /* verify */ "application/vnd.ms-excel.TimelineCache+xml": "TODO", /* verify */ @@ -146,6 +140,9 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-package.relationships+xml": "rels", "application/vnd.openxmlformats-officedocument.oleObject": "TODO", + /* Image */ + "image/png": "TODO", + "sheet": "js" }/*:any*/); @@ -161,11 +158,23 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, - sheets: { + sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" }, - styles: {/* Styles */ + charts: { /* Chartsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml", + xlsb: "application/vnd.ms-excel.chartsheet" + }, + dialogs: { /* Dialogsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml", + xlsb: "application/vnd.ms-excel.dialogsheet" + }, + macros: { /* Macrosheet (Excel 4.0 Macros) */ + xlsx: "application/vnd.ms-excel.macrosheet+xml", + xlsb: "application/vnd.ms-excel.macrosheet" + }, + styles: { /* Styles */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", xlsb: "application/vnd.ms-excel.styles" } @@ -180,9 +189,12 @@ var type2ct/*{[string]:Array}*/ = evert_arr(ct2type); XMLNS.CT = 'http://schemas.openxmlformats.org/package/2006/content-types'; function parse_ct(data/*:?string*/, opts) { - var ct = ({ workbooks: [], sheets: [], calcchains: [], themes: [], styles: [], - coreprops: [], extprops: [], custprops: [], strs:[], comments: [], vba: [], - TODO:[], rels:[], xmlns: "" }/*:any*/); + var ct = ({ + workbooks:[], sheets:[], charts:[], dialogs:[], + rels:[], strs:[], comments:[], + coreprops:[], extprops:[], custprops:[], themes:[], styles:[], + calcchains:[], vba: [], + TODO:[], xmlns: "" }/*:any*/); if(!data || !data.match) return ct; var ctext = {}; (data.match(tagregex)||[]).forEach(function(x) { diff --git a/bits/31_rels.js b/bits/31_rels.js index e8ebf3f..d3e345f 100644 --- a/bits/31_rels.js +++ b/bits/31_rels.js @@ -1,9 +1,15 @@ -/* 9.3.2 OPC Relationships Markup */ +/* 9.3 Relationships */ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument" }/*:any*/); +/* 9.3.3 Representing Relationships */ +function get_rels_path(file/*:string*/)/*:string*/ { + var n = file.lastIndexOf("/"); + return file.substr(0,n) + '/_rels' + file.substr(n) + ".rels"; +} + function parse_rels(data/*:?string*/, currentFilePath/*:string*/) { if (!data) return data; if (currentFilePath.charAt(0) !== '/') { @@ -11,28 +17,13 @@ function parse_rels(data/*:?string*/, currentFilePath/*:string*/) { } var rels = {}; var hash = {}; - var resolveRelativePathIntoAbsolute = function (to) { - var toksFrom = currentFilePath.split('/'); - toksFrom.pop(); // folder path - var toksTo = to.split('/'); - var reversed = []; - while (toksTo.length !== 0) { - var tokTo = toksTo.shift(); - if (tokTo === '..') { - toksFrom.pop(); - } else if (tokTo !== '.') { - toksFrom.push(tokTo); - } - } - return toksFrom.join('/'); - }; (data.match(tagregex)||[]).forEach(function(x) { var y = parsexmltag(x); /* 9.3.2.2 OPC_Relationships */ if (y[0] === '= 2) { o.dt = blob.read_shift(2); blob.l -= 2; } switch(o.BIFFVer) { case 0x0600: /* BIFF8 */ case 0x0500: /* BIFF5 */ @@ -147,6 +148,7 @@ function parse_BOF(blob, length) { break; default: if(length > 6) throw new Error("Unexpected BIFF Ver " + o.BIFFVer); } + blob.read_shift(length); return o; } @@ -156,7 +158,7 @@ function parse_BOF(blob, length) { function parse_InterfaceHdr(blob, length) { if(length === 0) return 0x04b0; var q; - if((q=blob.read_shift(2))!==0x04b0) throw 'InterfaceHdr codePage ' + q; + if((q=blob.read_shift(2))!==0x04b0) throw new Error("InterfaceHdr codePage " + q); return 0x04b0; } @@ -222,7 +224,7 @@ function parse_Row(blob, length) { /* 2.4.125 */ function parse_ForceFullCalculation(blob, length) { var header = parse_frtHeader(blob); - if(header.type != 0x08A3) throw "Invalid Future Record " + header.type; + if(header.type != 0x08A3) throw new Error("Invalid Future Record " + header.type); var fullcalc = blob.read_shift(4); return fullcalc !== 0x0; } @@ -311,9 +313,9 @@ function parse_MulRk(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var rkrecs = []; while(blob.l < target) rkrecs.push(parse_RkRec(blob)); - if(blob.l !== target) throw "MulRK read error"; + if(blob.l !== target) throw new Error("MulRK read error"); var lastcol = blob.read_shift(2); - if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch"; + if(rkrecs.length != lastcol - col + 1) throw new Error("MulRK length mismatch"); return {r:rw, c:col, C:lastcol, rkrec:rkrecs}; } /* 2.4.174 */ @@ -322,9 +324,9 @@ function parse_MulBlank(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var ixfes = []; while(blob.l < target) ixfes.push(blob.read_shift(2)); - if(blob.l !== target) throw "MulBlank read error"; + if(blob.l !== target) throw new Error("MulBlank read error"); var lastcol = blob.read_shift(2); - if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch"; + if(ixfes.length != lastcol - col + 1) throw new Error("MulBlank length mismatch"); return {r:rw, c:col, C:lastcol, ixfe:ixfes}; } @@ -388,7 +390,7 @@ function parse_Guts(blob, length) { var out = [blob.read_shift(2), blob.read_shift(2)]; if(out[0] !== 0) out[0]--; if(out[1] !== 0) out[1]--; - if(out[0] > 7 || out[1] > 7) throw "Bad Gutters: " + out.join("|"); + if(out[0] > 7 || out[1] > 7) throw new Error("Bad Gutters: " + out.join("|")); return out; } @@ -555,14 +557,14 @@ try { //var fmla = parse_ObjFmla(blob, s + length - blob.l); for(var i = 1; i < blob.lens.length-1; ++i) { - if(blob.l-s != blob.lens[i]) throw "TxO: bad continue record"; + if(blob.l-s != blob.lens[i]) throw new Error("TxO: bad continue record"); var hdr = blob[blob.l]; var t = parse_XLUnicodeStringNoCch(blob, blob.lens[i+1]-blob.lens[i]-1); texts += t; if(texts.length >= (hdr ? cchText : 2*cchText)) break; } if(texts.length !== cchText && texts.length !== cchText*2) { - throw "cchText: " + cchText + " != " + texts.length; + throw new Error("cchText: " + cchText + " != " + texts.length); } blob.l = s + length; @@ -570,9 +572,9 @@ try { // var rgTxoRuns = []; // for(var j = 0; j != cbRuns/8-1; ++j) blob.l += 8; // var cchText2 = blob.read_shift(2); -// if(cchText2 !== cchText) throw "TxOLastRun mismatch: " + cchText2 + " " + cchText; +// if(cchText2 !== cchText) throw new Error("TxOLastRun mismatch: " + cchText2 + " " + cchText); // blob.l += 6; -// if(s + length != blob.l) throw "TxO " + (s + length) + ", at " + blob.l; +// if(s + length != blob.l) throw new Error("TxO " + (s + length) + ", at " + blob.l); return { t: texts }; } catch(e) { blob.l = s + length; return { t: texts }; } } @@ -642,6 +644,15 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.261 */ +function parse_ShtProps(blob, length, opts) { + var def = {area:false}; + if(opts.biff != 5) { blob.l += length; return def; } + var d = blob.read_shift(1); blob.l += 3; + if((d & 0x10)) def.area = true; + return def; +} + var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -934,7 +945,6 @@ var parse_Surf = parsenoop; var parse_RadarArea = parsenoop; var parse_AxisParent = parsenoop; var parse_LegendException = parsenoop; -var parse_ShtProps = parsenoop; var parse_SerToCrt = parsenoop; var parse_AxesUsed = parsenoop; var parse_SBaseRef = parsenoop; diff --git a/bits/47_styxml.js b/bits/47_styxml.js index d161995..3125748 100644 --- a/bits/47_styxml.js +++ b/bits/47_styxml.js @@ -267,7 +267,7 @@ function parse_cellXfs(t, styles, opts) { /* 18.2.10 extLst CT_ExtensionList ? */ case '': break; case ']*r:id="([^"]*)"/)||["",""])[1]; + + return rels['!id'][id].Target; +} diff --git a/bits/66_wscommon.js b/bits/66_wscommon.js index b3ec93f..7e815ce 100644 --- a/bits/66_wscommon.js +++ b/bits/66_wscommon.js @@ -1,7 +1,10 @@ var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options -RELS.WS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"; +RELS.WS = [ + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", + "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet" +]; function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ { for(var i = 0, len = sst.length; i < len; ++i) if(sst[i].t === str) { sst.Count ++; return i; } diff --git a/bits/69_chartxml.js b/bits/69_chartxml.js new file mode 100644 index 0000000..dce140c --- /dev/null +++ b/bits/69_chartxml.js @@ -0,0 +1,41 @@ +function parse_numCache(data) { + var col = []; + + /* 21.2.2.150 pt CT_NumVal */ + (data.match(/(.*?)<\/c:pt>/mg)||[]).forEach(function(pt) { + var q = pt.match(/(.*)<\/c:v><\/c:pt>/); + if(!q) return; + col[+q[1]] = +q[2]; + }); + + /* 21.2.2.71 formatCode CT_Xstring */ + var nf = unescapexml((data.match(/(.*?)<\/c:formatCode>/) || ["","General"])[1]); + + return [col, nf]; +} + +/* 21.2 DrawingML - Charts */ +function parse_chart(data, name/*:string*/, opts, rels, wb, csheet) { + var cs = ((csheet || {"!type":"chart"})/*:any*/); + if(!data) return csheet; + /* 21.2.2.27 chart CT_Chart */ + + var C = 0, R = 0, col = "A"; + var refguess = {s: {r:2000000, c:2000000}, e: {r:0, c:0} }; + + /* 21.2.2.120 numCache CT_NumData */ + (data.match(/.*?<\/c:numCache>/gm)||[]).forEach(function(nc) { + var cache = parse_numCache(nc); + refguess.s.r = refguess.s.c = 0; + refguess.e.c = C; + col = encode_col(C); + cache[0].forEach(function(n,i) { + cs[col + encode_row(i)] = {t:'n', v:n, z:cache[1] }; + R = i; + }); + if(refguess.e.r < R) refguess.e.r = R; + ++C; + }); + if(C > 0) cs["!ref"] = encode_range(refguess); + return cs; +} diff --git a/bits/70_csheet.js b/bits/70_csheet.js new file mode 100644 index 0000000..49b66eb --- /dev/null +++ b/bits/70_csheet.js @@ -0,0 +1,47 @@ +RELS.CS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"; + +/* 18.3 Worksheets also covers Chartsheets */ +function parse_cs_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(!data) return data; + /* 18.3.1.12 chartsheet CT_ChartSheet */ + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var m; + + /* 18.3.1.36 drawing CT_Drawing */ + if((m = data.match(/drawing r:id="(.*?)"/))) s['!rel'] = m[1]; + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} + +/* [MS-XLSB] 2.1.7.7 Chart Sheet */ +function parse_cs_bin(data, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(!data) return data; + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var pass = false; + recordhopper(data, function cs_parse(val, Record) { + switch(Record.n) { + + case 'BrtDrawing': s['!rel'] = val; break; + + case 'BrtBeginSheet': break; + case 'BrtCsProp': break; // TODO + case 'BrtBeginCsViews': break; // TODO + case 'BrtBeginCsView': break; // TODO + case 'BrtEndCsView': break; // TODO + case 'BrtEndCsViews': break; // TODO + case 'BrtCsProtection': break; // TODO + case 'BrtMargins': break; // TODO + case 'BrtCsPageSetup': break; // TODO + case 'BrtEndSheet': break; // TODO + case 'BrtBeginHeaderFooter': break; // TODO + case 'BrtEndHeaderFooter': break; // TODO + default: if(!pass || opts.WTF) throw new Error("Unexpected record " + Record.n); + } + }, opts); + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} diff --git a/bits/74_xmlbin.js b/bits/74_xmlbin.js index 04000a0..c46ab11 100644 --- a/bits/74_xmlbin.js +++ b/bits/74_xmlbin.js @@ -8,6 +8,11 @@ function parse_ws(data, name/*:string*/, opts, rels, wb, themes, styles)/*:Works return parse_ws_xml((data/*:any*/), opts, rels, wb, themes, styles); } +function parse_cs(data, name/*:string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(name.slice(-4)===".bin") return parse_cs_bin((data/*:any*/), opts, rels, wb, themes, styles); + return parse_cs_xml((data/*:any*/), opts, rels, wb, themes, styles); +} + function parse_sty(data, name/*:string*/, themes, opts) { if(name.slice(-4)===".bin") return parse_sty_bin((data/*:any*/), themes, opts); return parse_sty_xml((data/*:any*/), themes, opts); diff --git a/bits/76_xls.js b/bits/76_xls.js index e58ca8c..f2f8420 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -260,7 +260,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'EOF': { if(--file_depth) break; if(range.e) { - out["!range"] = range; if(range.e.r > 0 && range.e.c > 0) { range.e.r--; range.e.c--; out["!ref"] = encode_range(range); @@ -295,6 +294,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { opts.snames.push(cur_sheet); } else cur_sheet = (Directory[s] || {name:""}).name; + if(val.dt == 0x20) out["!type"] = "chart"; mergecells = []; objects = []; array_formulae = []; opts.arrayf = array_formulae; @@ -304,6 +304,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { + if(out["!type"] == "chart" && out[encode_cell({c:val.c, r:val.r})]) ++val.c; temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); diff --git a/bits/77_parsetab.js b/bits/77_parsetab.js index b820f4a..072dbba 100644 --- a/bits/77_parsetab.js +++ b/bits/77_parsetab.js @@ -475,7 +475,7 @@ var XLSBRecordEnum = { /*::[*/0x021E/*::]*/: { n:"BrtBeginECTwFldInfo", f:parsenoop }, /*::[*/0x0224/*::]*/: { n:"BrtFileSharing", f:parsenoop }, /*::[*/0x0225/*::]*/: { n:"BrtOleSize", f:parsenoop }, - /*::[*/0x0226/*::]*/: { n:"BrtDrawing", f:parsenoop }, + /*::[*/0x0226/*::]*/: { n:"BrtDrawing", f:parse_RelID }, /*::[*/0x0227/*::]*/: { n:"BrtLegacyDrawing", f:parsenoop }, /*::[*/0x0228/*::]*/: { n:"BrtLegacyDrawingHF", f:parsenoop }, /*::[*/0x0229/*::]*/: { n:"BrtWebOpt", f:parsenoop }, diff --git a/bits/85_parsezip.js b/bits/85_parsezip.js index afb7c10..a56a646 100644 --- a/bits/85_parsezip.js +++ b/bits/85_parsezip.js @@ -1,15 +1,37 @@ function safe_parse_wbrels(wbrels, sheets) { if(!wbrels) return 0; + function get_type(n) { + if(RELS.WS.indexOf(n) > -1) return "sheet"; + if(RELS.CS && n == RELS.CS) return "chart"; + if(RELS.DS && n == RELS.DS) return "dialog"; + if(RELS.MS && n == RELS.MS) return "macro"; + if(!n || !n.length) return "sheet"; + return n; + } try { - wbrels = sheets.map(function pwbr(w) { return [w.name, wbrels['!id'][w.id].Target]; }); + wbrels = sheets.map(function pwbr(w) { if(!w.id) w.id = w.strRelID; return [w.name, wbrels['!id'][w.id].Target, get_type(wbrels['!id'][w.id].Type)]; }); } catch(e) { return null; } return !wbrels || wbrels.length === 0 ? null : wbrels; } -function safe_parse_ws(zip, path/*:string*/, relsPath/*:string*/, sheet, sheetRels, sheets, opts, wb, themes, styles) { +function safe_parse_sheet(zip, path/*:string*/, relsPath/*:string*/, sheet, sheetRels, sheets, stype/*:string*/, opts, wb, themes, styles) { try { sheetRels[sheet]=parse_rels(getzipstr(zip, relsPath, true), path); - sheets[sheet]=parse_ws(getzipdata(zip, path),path,opts,sheetRels[sheet], wb, themes, styles); + var data = getzipdata(zip, path); + switch(stype) { + case 'sheet': sheets[sheet]=parse_ws(data, path, opts,sheetRels[sheet], wb, themes, styles); break; + case 'chart': + var cs = parse_cs(data, path, opts,sheetRels[sheet], wb, themes, styles); + sheets[sheet] = cs; + if(!cs || !cs['!chart']) break; + var dfile = resolve_path(cs['!chart'].Target, path); + var drelsp = get_rels_path(dfile); + var draw = parse_drawing(getzipstr(zip, dfile, true), parse_rels(getzipstr(zip,drelsp,true), dfile)); + var chartp = resolve_path(draw, dfile); + var crelsp = get_rels_path(chartp); + cs = parse_chart(getzipstr(zip, chartp, true), chartp, opts, parse_rels(getzipstr(zip, crelsp,true), chartp), wb, cs); + break; + } } catch(e) { if(opts.WTF) throw e; } } @@ -76,8 +98,8 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { var out = ({}/*:any*/); if(opts.bookSheets || opts.bookProps) { - if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; - else if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + else if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; if(opts.bookProps) { out.Props = props; out.Custprops = custprops; } if(opts.bookSheets && typeof sheets !== 'undefined') out.SheetNames = sheets; if(opts.bookSheets ? out.SheetNames : opts.bookProps) return out; @@ -106,13 +128,16 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { /* Numbers iOS hack */ var nmode = (getzipdata(zip,"xl/worksheets/sheet.xml",true))?1:0; for(i = 0; i != props.Worksheets; ++i) { - if(wbrels && wbrels[i]) path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); - else { + var stype = "sheet"; + if(wbrels && wbrels[i]) { + path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); + stype = wbrels[i][2]; + } else { path = 'xl/worksheets/sheet'+(i+1-nmode)+"." + wbext; path = path.replace(/sheet0\./,"sheet."); } relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); - safe_parse_ws(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, opts, wb, themes, styles); + safe_parse_sheet(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, stype, opts, wb, themes, styles); } if(dir.comments) parse_comments(zip, dir.comments, sheets, sheetRels, opts); diff --git a/bits/86_writezip.js b/bits/86_writezip.js index f89632e..97d2a21 100644 --- a/bits/86_writezip.js +++ b/bits/86_writezip.js @@ -65,7 +65,7 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/worksheets/sheet" + rId + "." + wbext; zip.file(f, write_ws(rId-1, f, opts, wb)); ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS); + add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); } if(opts.Strings != null && opts.Strings.length > 0) { diff --git a/docbits/54_shobject.md b/docbits/54_shobject.md new file mode 100644 index 0000000..c3d76b6 --- /dev/null +++ b/docbits/54_shobject.md @@ -0,0 +1,7 @@ +### Chartsheet Object + +Chartsheets are represented as standard worksheets. They are distinguished with +the `!type` property set to `"chart"`. + +The underlying data and `!ref` refer to the cached data in the chartsheet. + diff --git a/docbits/54_wbobject.md b/docbits/55_wbobject.md similarity index 100% rename from docbits/54_wbobject.md rename to docbits/55_wbobject.md diff --git a/docbits/81_writeopts.md b/docbits/81_writeopts.md index 205af24..d1c5ef9 100644 --- a/docbits/81_writeopts.md +++ b/docbits/81_writeopts.md @@ -13,8 +13,8 @@ The exported `write` and `writeFile` functions accept an options argument: - `bookSST` is slower and more memory intensive, but has better compatibility with older versions of iOS Numbers -- The raw data is the only thing guaranteed to be saved. Formulae, formatting, - and other niceties may not be serialized (pending CSF standardization) +- The raw data is the only thing guaranteed to be saved. Features not described + in this README may not be serialized. - `cellDates` only applies to XLSX output and is not guaranteed to work with third-party readers. Excel itself does not usually write cells with type `d` so non-Excel tools may ignore the data or blow up in the presence of dates. diff --git a/test.js b/test.js index 1c2f757..a2b0e7d 100644 --- a/test.js +++ b/test.js @@ -88,15 +88,17 @@ function parsetest(x, wb, full, ext) { ext = (ext ? " [" + ext + "]": ""); if(!full && ext) return; describe(x + ext + ' should have all bits', function() { - var sname = dir + '2011/' + x.substr(x.lastIndexOf('/')+1) + '.sheetnames'; + var sname = dir + '2016/' + x.substr(x.lastIndexOf('/')+1) + '.sheetnames'; + if(!fs.existsSync(sname)) sname = dir + '2011/' + x.substr(x.lastIndexOf('/')+1) + '.sheetnames'; + if(!fs.existsSync(sname)) sname = dir + '2013/' + x.substr(x.lastIndexOf('/')+1) + '.sheetnames'; it('should have all sheets', function() { wb.SheetNames.forEach(function(y) { assert(wb.Sheets[y], 'bad sheet ' + y); }); }); - it('should have the right sheet names', fs.existsSync(sname) ? function() { + if(fs.existsSync(sname)) it('should have the right sheet names', function() { var file = fs.readFileSync(sname, 'utf-8').replace(/\r/g,""); var names = wb.SheetNames.map(fixsheetname).join("\n") + "\n"; - assert.equal(names, file); - } : null); + if(file.length) assert.equal(names, file); + }); }); describe(x + ext + ' should generate CSV', function() { wb.SheetNames.forEach(function(ws, i) { @@ -140,11 +142,11 @@ function parsetest(x, wb, full, ext) { describe(x + ext + ' should generate correct CSV output', function() { wb.SheetNames.forEach(function(ws, i) { var name = getfile(dir, x, i, ".csv"); - it('#' + i + ' (' + ws + ')', fs.existsSync(name) ? function() { + if(fs.existsSync(name)) it('#' + i + ' (' + ws + ')', function() { var file = fs.readFileSync(name, 'utf-8'); var csv = X.utils.make_csv(wb.Sheets[ws]); assert.equal(fixcsv(csv), fixcsv(file), "CSV badness"); - } : null); + }); }); }); describe(x + ext + ' should generate correct JSON output', function() { @@ -188,8 +190,8 @@ var wbtable = {}; describe('should parse test files', function() { files.forEach(function(x) { - if(!fs.existsSync(dir + x)) return; - it(x, x.substr(-8) == ".pending" ? null : function() { + if(x.slice(-8) == ".pending" || !fs.existsSync(dir + x)) return; + it(x, function() { var wb = X.readFile(dir + x, opts); wbtable[dir + x] = wb; parsetest(x, wb, true); @@ -206,8 +208,8 @@ describe('should parse test files', function() { }); }); fileA.forEach(function(x) { - if(!fs.existsSync(dir + x)) return; - it(x, x.substr(-8) == ".pending" ? null : function() { + if(x.slice(-8) == ".pending" || !fs.existsSync(dir + x)) return; + it(x, function() { var wb = X.readFile(dir + x, {WTF:opts.wtf, sheetRows:10}); parsetest(x, wb, false); }); diff --git a/test_files b/test_files index 7fe887f..cbafd3a 160000 --- a/test_files +++ b/test_files @@ -1 +1 @@ -Subproject commit 7fe887ff7b26b233db24f3d4df513319c493efc8 +Subproject commit cbafd3a184d0c668aaae1f9f8420ccaed74ad8e5 diff --git a/tests.lst b/tests.lst index 28b2521..d03ffbd 100644 --- a/tests.lst +++ b/tests.lst @@ -1,15 +1,24 @@ AutoFilter.xlsb -BlankSheetTypes.xlsb.pending +BlankSheetTypes.xlsb.pending # macrosheets ErrorTypes.xlsb NumberFormatCondition.xlsb RkNumber.xlsb -apachepoi_Simple.xlsb.pending +apachepoi_51519.xlsb +# apachepoi_Simple.xlsb # Beta file +apachepoi_WithTextBox.xlsb +apachepoi_comments.xlsb +apachepoi_date.xlsb +apachepoi_hyperlink.xlsb +apachepoi_protected_passtika.xlsb.pending +apachepoi_sample.xlsb +apachepoi_testVarious.xlsb calendar_stress_test.xlsb.pending cell_style_simple.xlsb +column_width.xlsb comments_stress_test.xlsb custom_properties.xlsb defined_names_simple.xlsb -# formula_stress_test.xlsb # xlml +formula_stress_test.xlsb formulae_test_simple.xlsb hyperlink_no_rels.xlsb hyperlink_stress_test_2011.xlsb @@ -22,8 +31,10 @@ number_format_russian.xlsb numfmt_1_russian.xlsb phonetic_text.xlsb pivot_table_named_range.xlsb -# pivot_table_test.xlsb # xlml +pivot_table_test.xlsb rich_text_stress.xlsb +row_height.xlsb +sized_boxen.xlsb smart_tags_2007.xlsb sushi.xlsb text_and_numbers.xlsb @@ -226,7 +237,7 @@ apachepoi_Themes2.xlsx apachepoi_TwoSheetsNoneHidden.xlsx apachepoi_TwoSheetsOneHidden.xlsx apachepoi_WithChart.xlsx -apachepoi_WithChartSheet.xlsx.pending +apachepoi_WithChartSheet.xlsx apachepoi_WithConditionalFormatting.xlsx apachepoi_WithDrawing.xlsx apachepoi_WithEmbeded.xlsx @@ -242,7 +253,7 @@ apachepoi_atp.xlsx apachepoi_bug60858.xlsx apachepoi_chartTitle_noTitle.xlsx apachepoi_chartTitle_withTitle.xlsx -apachepoi_chart_sheet.xlsx.pending +apachepoi_chart_sheet.xlsx apachepoi_commentTest.xlsx apachepoi_comments.xlsx apachepoi_craftonhills.edu_programreview_report.aspx_goalpriorityreport_0011d159-1eeb-4b63-8833-867b0926e5f3.xlsx @@ -343,7 +354,7 @@ openpyxl_r_bug304.xlsx.pending openpyxl_r_comments.xlsx openpyxl_r_complex-styles.xlsx openpyxl_r_conditional-formatting.xlsx -openpyxl_r_contains_chartsheets.xlsx.pending +openpyxl_r_contains_chartsheets.xlsx openpyxl_r_date_1900.xlsx openpyxl_r_date_1904.xlsx openpyxl_r_formulae.xlsx diff --git a/xlsx.flow.js b/xlsx.flow.js index 1b6572d..843fe7f 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -1533,6 +1533,18 @@ if (typeof exports !== 'undefined') { _fs = require('fs'); } } + +function resolve_path(path/*:string*/, base/*:string*/)/*:string*/ { + var result = base.split('/'); + if(base.slice(-1) != "/") result.pop(); // folder path + var target = path.split('/'); + while (target.length !== 0) { + var step = target.shift(); + if (step === '..') result.pop(); + else if (step !== '.') result.push(step); + } + return result.join('/'); +} var attregexg=/([^\s?>\/]+)=((?:")([^"]*)(?:")|(?:')([^']*)(?:'))/g; var tagregex=/<[^>]*>/g; var nsregex=/<\w*:/, nsregex2 = /<(\/?)\w+:/; @@ -2552,17 +2564,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Worksheet */ "application/vnd.ms-excel.binIndexWs": "TODO", /* Binary Index */ - /* Chartsheet */ - "application/vnd.ms-excel.chartsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": "TODO", - - /* Dialogsheet */ - "application/vnd.ms-excel.dialogsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": "TODO", - /* Macrosheet */ - "application/vnd.ms-excel.macrosheet": "TODO", - "application/vnd.ms-excel.macrosheet+xml": "TODO", "application/vnd.ms-excel.intlmacrosheet": "TODO", "application/vnd.ms-excel.binIndexMs": "TODO", /* Binary Index */ @@ -2573,6 +2575,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Custom Data Properties */ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", /* Comments */ "application/vnd.ms-excel.comments": "comments", @@ -2652,6 +2655,9 @@ var ct2type/*{[string]:string}*/ = ({ /* Themes */ "application/vnd.openxmlformats-officedocument.theme+xml": "themes", + /* Theme Override */ + "application/vnd.openxmlformats-officedocument.themeOverride+xml": "TODO", + /* Timeline */ "application/vnd.ms-excel.Timeline+xml": "TODO", /* verify */ "application/vnd.ms-excel.TimelineCache+xml": "TODO", /* verify */ @@ -2688,6 +2694,9 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-package.relationships+xml": "rels", "application/vnd.openxmlformats-officedocument.oleObject": "TODO", + /* Image */ + "image/png": "TODO", + "sheet": "js" }/*:any*/); @@ -2703,11 +2712,23 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, - sheets: { + sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" }, - styles: {/* Styles */ + charts: { /* Chartsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml", + xlsb: "application/vnd.ms-excel.chartsheet" + }, + dialogs: { /* Dialogsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml", + xlsb: "application/vnd.ms-excel.dialogsheet" + }, + macros: { /* Macrosheet (Excel 4.0 Macros) */ + xlsx: "application/vnd.ms-excel.macrosheet+xml", + xlsb: "application/vnd.ms-excel.macrosheet" + }, + styles: { /* Styles */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", xlsb: "application/vnd.ms-excel.styles" } @@ -2722,9 +2743,12 @@ var type2ct/*{[string]:Array}*/ = evert_arr(ct2type); XMLNS.CT = 'http://schemas.openxmlformats.org/package/2006/content-types'; function parse_ct(data/*:?string*/, opts) { - var ct = ({ workbooks: [], sheets: [], calcchains: [], themes: [], styles: [], - coreprops: [], extprops: [], custprops: [], strs:[], comments: [], vba: [], - TODO:[], rels:[], xmlns: "" }/*:any*/); + var ct = ({ + workbooks:[], sheets:[], charts:[], dialogs:[], + rels:[], strs:[], comments:[], + coreprops:[], extprops:[], custprops:[], themes:[], styles:[], + calcchains:[], vba: [], + TODO:[], xmlns: "" }/*:any*/); if(!data || !data.match) return ct; var ctext = {}; (data.match(tagregex)||[]).forEach(function(x) { @@ -2799,12 +2823,18 @@ function write_ct(ct, opts)/*:string*/ { if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } -/* 9.3.2 OPC Relationships Markup */ +/* 9.3 Relationships */ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument" }/*:any*/); +/* 9.3.3 Representing Relationships */ +function get_rels_path(file/*:string*/)/*:string*/ { + var n = file.lastIndexOf("/"); + return file.substr(0,n) + '/_rels' + file.substr(n) + ".rels"; +} + function parse_rels(data/*:?string*/, currentFilePath/*:string*/) { if (!data) return data; if (currentFilePath.charAt(0) !== '/') { @@ -2812,28 +2842,13 @@ function parse_rels(data/*:?string*/, currentFilePath/*:string*/) { } var rels = {}; var hash = {}; - var resolveRelativePathIntoAbsolute = function (to) { - var toksFrom = currentFilePath.split('/'); - toksFrom.pop(); // folder path - var toksTo = to.split('/'); - var reversed = []; - while (toksTo.length !== 0) { - var tokTo = toksTo.shift(); - if (tokTo === '..') { - toksFrom.pop(); - } else if (tokTo !== '.') { - toksFrom.push(tokTo); - } - } - return toksFrom.join('/'); - }; (data.match(tagregex)||[]).forEach(function(x) { var y = parsexmltag(x); /* 9.3.2.2 OPC_Relationships */ if (y[0] === '= 2) { o.dt = blob.read_shift(2); blob.l -= 2; } switch(o.BIFFVer) { case 0x0600: /* BIFF8 */ case 0x0500: /* BIFF5 */ @@ -3811,6 +3837,7 @@ function parse_BOF(blob, length) { break; default: if(length > 6) throw new Error("Unexpected BIFF Ver " + o.BIFFVer); } + blob.read_shift(length); return o; } @@ -3820,7 +3847,7 @@ function parse_BOF(blob, length) { function parse_InterfaceHdr(blob, length) { if(length === 0) return 0x04b0; var q; - if((q=blob.read_shift(2))!==0x04b0) throw 'InterfaceHdr codePage ' + q; + if((q=blob.read_shift(2))!==0x04b0) throw new Error("InterfaceHdr codePage " + q); return 0x04b0; } @@ -3886,7 +3913,7 @@ function parse_Row(blob, length) { /* 2.4.125 */ function parse_ForceFullCalculation(blob, length) { var header = parse_frtHeader(blob); - if(header.type != 0x08A3) throw "Invalid Future Record " + header.type; + if(header.type != 0x08A3) throw new Error("Invalid Future Record " + header.type); var fullcalc = blob.read_shift(4); return fullcalc !== 0x0; } @@ -3975,9 +4002,9 @@ function parse_MulRk(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var rkrecs = []; while(blob.l < target) rkrecs.push(parse_RkRec(blob)); - if(blob.l !== target) throw "MulRK read error"; + if(blob.l !== target) throw new Error("MulRK read error"); var lastcol = blob.read_shift(2); - if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch"; + if(rkrecs.length != lastcol - col + 1) throw new Error("MulRK length mismatch"); return {r:rw, c:col, C:lastcol, rkrec:rkrecs}; } /* 2.4.174 */ @@ -3986,9 +4013,9 @@ function parse_MulBlank(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var ixfes = []; while(blob.l < target) ixfes.push(blob.read_shift(2)); - if(blob.l !== target) throw "MulBlank read error"; + if(blob.l !== target) throw new Error("MulBlank read error"); var lastcol = blob.read_shift(2); - if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch"; + if(ixfes.length != lastcol - col + 1) throw new Error("MulBlank length mismatch"); return {r:rw, c:col, C:lastcol, ixfe:ixfes}; } @@ -4052,7 +4079,7 @@ function parse_Guts(blob, length) { var out = [blob.read_shift(2), blob.read_shift(2)]; if(out[0] !== 0) out[0]--; if(out[1] !== 0) out[1]--; - if(out[0] > 7 || out[1] > 7) throw "Bad Gutters: " + out.join("|"); + if(out[0] > 7 || out[1] > 7) throw new Error("Bad Gutters: " + out.join("|")); return out; } @@ -4219,14 +4246,14 @@ try { //var fmla = parse_ObjFmla(blob, s + length - blob.l); for(var i = 1; i < blob.lens.length-1; ++i) { - if(blob.l-s != blob.lens[i]) throw "TxO: bad continue record"; + if(blob.l-s != blob.lens[i]) throw new Error("TxO: bad continue record"); var hdr = blob[blob.l]; var t = parse_XLUnicodeStringNoCch(blob, blob.lens[i+1]-blob.lens[i]-1); texts += t; if(texts.length >= (hdr ? cchText : 2*cchText)) break; } if(texts.length !== cchText && texts.length !== cchText*2) { - throw "cchText: " + cchText + " != " + texts.length; + throw new Error("cchText: " + cchText + " != " + texts.length); } blob.l = s + length; @@ -4234,9 +4261,9 @@ try { // var rgTxoRuns = []; // for(var j = 0; j != cbRuns/8-1; ++j) blob.l += 8; // var cchText2 = blob.read_shift(2); -// if(cchText2 !== cchText) throw "TxOLastRun mismatch: " + cchText2 + " " + cchText; +// if(cchText2 !== cchText) throw new Error("TxOLastRun mismatch: " + cchText2 + " " + cchText); // blob.l += 6; -// if(s + length != blob.l) throw "TxO " + (s + length) + ", at " + blob.l; +// if(s + length != blob.l) throw new Error("TxO " + (s + length) + ", at " + blob.l); return { t: texts }; } catch(e) { blob.l = s + length; return { t: texts }; } } @@ -4306,6 +4333,15 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.261 */ +function parse_ShtProps(blob, length, opts) { + var def = {area:false}; + if(opts.biff != 5) { blob.l += length; return def; } + var d = blob.read_shift(1); blob.l += 3; + if((d & 0x10)) def.area = true; + return def; +} + var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -4598,7 +4634,6 @@ var parse_Surf = parsenoop; var parse_RadarArea = parsenoop; var parse_AxisParent = parsenoop; var parse_LegendException = parsenoop; -var parse_ShtProps = parsenoop; var parse_SerToCrt = parsenoop; var parse_AxesUsed = parsenoop; var parse_SBaseRef = parsenoop; @@ -5498,7 +5533,7 @@ function parse_cellXfs(t, styles, opts) { /* 18.2.10 extLst CT_ExtensionList ? */ case '': break; case ']*r:id="([^"]*)"/)||["",""])[1]; + + return rels['!id'][id].Target; +} function parse_comments(zip, dirComments, sheets, sheetRels, opts) { for(var i = 0; i != dirComments.length; ++i) { @@ -8555,7 +8608,10 @@ function csf_to_ods_formula(f/*:string*/)/*:string*/ { var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options -RELS.WS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"; +RELS.WS = [ + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", + "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet" +]; function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ { for(var i = 0, len = sst.length; i < len; ++i) if(sst[i].t === str) { sst.Count ++; return i; } @@ -9581,6 +9637,94 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/) { write_record(ba, "BrtEndSheet"); return ba.end(); } +function parse_numCache(data) { + var col = []; + + /* 21.2.2.150 pt CT_NumVal */ + (data.match(/(.*?)<\/c:pt>/mg)||[]).forEach(function(pt) { + var q = pt.match(/(.*)<\/c:v><\/c:pt>/); + if(!q) return; + col[+q[1]] = +q[2]; + }); + + /* 21.2.2.71 formatCode CT_Xstring */ + var nf = unescapexml((data.match(/(.*?)<\/c:formatCode>/) || ["","General"])[1]); + + return [col, nf]; +} + +/* 21.2 DrawingML - Charts */ +function parse_chart(data, name/*:string*/, opts, rels, wb, csheet) { + var cs = ((csheet || {"!type":"chart"})/*:any*/); + if(!data) return csheet; + /* 21.2.2.27 chart CT_Chart */ + + var C = 0, R = 0, col = "A"; + var refguess = {s: {r:2000000, c:2000000}, e: {r:0, c:0} }; + + /* 21.2.2.120 numCache CT_NumData */ + (data.match(/.*?<\/c:numCache>/gm)||[]).forEach(function(nc) { + var cache = parse_numCache(nc); + refguess.s.r = refguess.s.c = 0; + refguess.e.c = C; + col = encode_col(C); + cache[0].forEach(function(n,i) { + cs[col + encode_row(i)] = {t:'n', v:n, z:cache[1] }; + R = i; + }); + if(refguess.e.r < R) refguess.e.r = R; + ++C; + }); + if(C > 0) cs["!ref"] = encode_range(refguess); + return cs; +} +RELS.CS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"; + +/* 18.3 Worksheets also covers Chartsheets */ +function parse_cs_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(!data) return data; + /* 18.3.1.12 chartsheet CT_ChartSheet */ + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var m; + + /* 18.3.1.36 drawing CT_Drawing */ + if((m = data.match(/drawing r:id="(.*?)"/))) s['!rel'] = m[1]; + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} + +/* [MS-XLSB] 2.1.7.7 Chart Sheet */ +function parse_cs_bin(data, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(!data) return data; + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var pass = false; + recordhopper(data, function cs_parse(val, Record) { + switch(Record.n) { + + case 'BrtDrawing': s['!rel'] = val; break; + + case 'BrtBeginSheet': break; + case 'BrtCsProp': break; // TODO + case 'BrtBeginCsViews': break; // TODO + case 'BrtBeginCsView': break; // TODO + case 'BrtEndCsView': break; // TODO + case 'BrtEndCsViews': break; // TODO + case 'BrtCsProtection': break; // TODO + case 'BrtMargins': break; // TODO + case 'BrtCsPageSetup': break; // TODO + case 'BrtEndSheet': break; // TODO + case 'BrtBeginHeaderFooter': break; // TODO + case 'BrtEndHeaderFooter': break; // TODO + default: if(!pass || opts.WTF) throw new Error("Unexpected record " + Record.n); + } + }, opts); + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} /* 18.2.28 (CT_WorkbookProtection) Defaults */ var WBPropsDef = [ ['allowRefreshQuery', '0'], @@ -10072,6 +10216,11 @@ function parse_ws(data, name/*:string*/, opts, rels, wb, themes, styles)/*:Works return parse_ws_xml((data/*:any*/), opts, rels, wb, themes, styles); } +function parse_cs(data, name/*:string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { + if(name.slice(-4)===".bin") return parse_cs_bin((data/*:any*/), opts, rels, wb, themes, styles); + return parse_cs_xml((data/*:any*/), opts, rels, wb, themes, styles); +} + function parse_sty(data, name/*:string*/, themes, opts) { if(name.slice(-4)===".bin") return parse_sty_bin((data/*:any*/), themes, opts); return parse_sty_xml((data/*:any*/), themes, opts); @@ -11255,7 +11404,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'EOF': { if(--file_depth) break; if(range.e) { - out["!range"] = range; if(range.e.r > 0 && range.e.c > 0) { range.e.r--; range.e.c--; out["!ref"] = encode_range(range); @@ -11290,6 +11438,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { opts.snames.push(cur_sheet); } else cur_sheet = (Directory[s] || {name:""}).name; + if(val.dt == 0x20) out["!type"] = "chart"; mergecells = []; objects = []; array_formulae = []; opts.arrayf = array_formulae; @@ -11299,6 +11448,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { + if(out["!type"] == "chart" && out[encode_cell({c:val.c, r:val.r})]) ++val.c; temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); @@ -12225,7 +12375,7 @@ var XLSBRecordEnum = { /*::[*/0x021E/*::]*/: { n:"BrtBeginECTwFldInfo", f:parsenoop }, /*::[*/0x0224/*::]*/: { n:"BrtFileSharing", f:parsenoop }, /*::[*/0x0225/*::]*/: { n:"BrtOleSize", f:parsenoop }, - /*::[*/0x0226/*::]*/: { n:"BrtDrawing", f:parsenoop }, + /*::[*/0x0226/*::]*/: { n:"BrtDrawing", f:parse_RelID }, /*::[*/0x0227/*::]*/: { n:"BrtLegacyDrawing", f:parsenoop }, /*::[*/0x0228/*::]*/: { n:"BrtLegacyDrawingHF", f:parsenoop }, /*::[*/0x0229/*::]*/: { n:"BrtWebOpt", f:parsenoop }, @@ -13702,16 +13852,38 @@ var fix_write_opts = fix_opts_func([ ]); function safe_parse_wbrels(wbrels, sheets) { if(!wbrels) return 0; + function get_type(n) { + if(RELS.WS.indexOf(n) > -1) return "sheet"; + if(RELS.CS && n == RELS.CS) return "chart"; + if(RELS.DS && n == RELS.DS) return "dialog"; + if(RELS.MS && n == RELS.MS) return "macro"; + if(!n || !n.length) return "sheet"; + return n; + } try { - wbrels = sheets.map(function pwbr(w) { return [w.name, wbrels['!id'][w.id].Target]; }); + wbrels = sheets.map(function pwbr(w) { if(!w.id) w.id = w.strRelID; return [w.name, wbrels['!id'][w.id].Target, get_type(wbrels['!id'][w.id].Type)]; }); } catch(e) { return null; } return !wbrels || wbrels.length === 0 ? null : wbrels; } -function safe_parse_ws(zip, path/*:string*/, relsPath/*:string*/, sheet, sheetRels, sheets, opts, wb, themes, styles) { +function safe_parse_sheet(zip, path/*:string*/, relsPath/*:string*/, sheet, sheetRels, sheets, stype/*:string*/, opts, wb, themes, styles) { try { sheetRels[sheet]=parse_rels(getzipstr(zip, relsPath, true), path); - sheets[sheet]=parse_ws(getzipdata(zip, path),path,opts,sheetRels[sheet], wb, themes, styles); + var data = getzipdata(zip, path); + switch(stype) { + case 'sheet': sheets[sheet]=parse_ws(data, path, opts,sheetRels[sheet], wb, themes, styles); break; + case 'chart': + var cs = parse_cs(data, path, opts,sheetRels[sheet], wb, themes, styles); + sheets[sheet] = cs; + if(!cs || !cs['!chart']) break; + var dfile = resolve_path(cs['!chart'].Target, path); + var drelsp = get_rels_path(dfile); + var draw = parse_drawing(getzipstr(zip, dfile, true), parse_rels(getzipstr(zip,drelsp,true), dfile)); + var chartp = resolve_path(draw, dfile); + var crelsp = get_rels_path(chartp); + cs = parse_chart(getzipstr(zip, chartp, true), chartp, opts, parse_rels(getzipstr(zip, crelsp,true), chartp), wb, cs); + break; + } } catch(e) { if(opts.WTF) throw e; } } @@ -13778,8 +13950,8 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { var out = ({}/*:any*/); if(opts.bookSheets || opts.bookProps) { - if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; - else if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + else if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; if(opts.bookProps) { out.Props = props; out.Custprops = custprops; } if(opts.bookSheets && typeof sheets !== 'undefined') out.SheetNames = sheets; if(opts.bookSheets ? out.SheetNames : opts.bookProps) return out; @@ -13808,13 +13980,16 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { /* Numbers iOS hack */ var nmode = (getzipdata(zip,"xl/worksheets/sheet.xml",true))?1:0; for(i = 0; i != props.Worksheets; ++i) { - if(wbrels && wbrels[i]) path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); - else { + var stype = "sheet"; + if(wbrels && wbrels[i]) { + path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); + stype = wbrels[i][2]; + } else { path = 'xl/worksheets/sheet'+(i+1-nmode)+"." + wbext; path = path.replace(/sheet0\./,"sheet."); } relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); - safe_parse_ws(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, opts, wb, themes, styles); + safe_parse_sheet(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, stype, opts, wb, themes, styles); } if(dir.comments) parse_comments(zip, dir.comments, sheets, sheetRels, opts); @@ -13909,7 +14084,7 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/worksheets/sheet" + rId + "." + wbext; zip.file(f, write_ws(rId-1, f, opts, wb)); ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS); + add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); } if(opts.Strings != null && opts.Strings.length > 0) { diff --git a/xlsx.js b/xlsx.js index 8ca5cc0..b95fe71 100644 --- a/xlsx.js +++ b/xlsx.js @@ -1484,6 +1484,18 @@ if (typeof exports !== 'undefined') { _fs = require('fs'); } } + +function resolve_path(path, base) { + var result = base.split('/'); + if(base.slice(-1) != "/") result.pop(); // folder path + var target = path.split('/'); + while (target.length !== 0) { + var step = target.shift(); + if (step === '..') result.pop(); + else if (step !== '.') result.push(step); + } + return result.join('/'); +} var attregexg=/([^\s?>\/]+)=((?:")([^"]*)(?:")|(?:')([^']*)(?:'))/g; var tagregex=/<[^>]*>/g; var nsregex=/<\w*:/, nsregex2 = /<(\/?)\w+:/; @@ -2500,17 +2512,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Worksheet */ "application/vnd.ms-excel.binIndexWs": "TODO", /* Binary Index */ - /* Chartsheet */ - "application/vnd.ms-excel.chartsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": "TODO", - - /* Dialogsheet */ - "application/vnd.ms-excel.dialogsheet": "TODO", - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": "TODO", - /* Macrosheet */ - "application/vnd.ms-excel.macrosheet": "TODO", - "application/vnd.ms-excel.macrosheet+xml": "TODO", "application/vnd.ms-excel.intlmacrosheet": "TODO", "application/vnd.ms-excel.binIndexMs": "TODO", /* Binary Index */ @@ -2521,6 +2523,7 @@ var ct2type/*{[string]:string}*/ = ({ /* Custom Data Properties */ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", /* Comments */ "application/vnd.ms-excel.comments": "comments", @@ -2600,6 +2603,9 @@ var ct2type/*{[string]:string}*/ = ({ /* Themes */ "application/vnd.openxmlformats-officedocument.theme+xml": "themes", + /* Theme Override */ + "application/vnd.openxmlformats-officedocument.themeOverride+xml": "TODO", + /* Timeline */ "application/vnd.ms-excel.Timeline+xml": "TODO", /* verify */ "application/vnd.ms-excel.TimelineCache+xml": "TODO", /* verify */ @@ -2636,6 +2642,9 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-package.relationships+xml": "rels", "application/vnd.openxmlformats-officedocument.oleObject": "TODO", + /* Image */ + "image/png": "TODO", + "sheet": "js" }); @@ -2651,11 +2660,23 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, - sheets: { + sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" }, - styles: {/* Styles */ + charts: { /* Chartsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml", + xlsb: "application/vnd.ms-excel.chartsheet" + }, + dialogs: { /* Dialogsheet */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml", + xlsb: "application/vnd.ms-excel.dialogsheet" + }, + macros: { /* Macrosheet (Excel 4.0 Macros) */ + xlsx: "application/vnd.ms-excel.macrosheet+xml", + xlsb: "application/vnd.ms-excel.macrosheet" + }, + styles: { /* Styles */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", xlsb: "application/vnd.ms-excel.styles" } @@ -2670,9 +2691,12 @@ var type2ct/*{[string]:Array}*/ = evert_arr(ct2type); XMLNS.CT = 'http://schemas.openxmlformats.org/package/2006/content-types'; function parse_ct(data, opts) { - var ct = ({ workbooks: [], sheets: [], calcchains: [], themes: [], styles: [], - coreprops: [], extprops: [], custprops: [], strs:[], comments: [], vba: [], - TODO:[], rels:[], xmlns: "" }); + var ct = ({ + workbooks:[], sheets:[], charts:[], dialogs:[], + rels:[], strs:[], comments:[], + coreprops:[], extprops:[], custprops:[], themes:[], styles:[], + calcchains:[], vba: [], + TODO:[], xmlns: "" }); if(!data || !data.match) return ct; var ctext = {}; (data.match(tagregex)||[]).forEach(function(x) { @@ -2747,12 +2771,18 @@ function write_ct(ct, opts) { if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } -/* 9.3.2 OPC Relationships Markup */ +/* 9.3 Relationships */ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument" }); +/* 9.3.3 Representing Relationships */ +function get_rels_path(file) { + var n = file.lastIndexOf("/"); + return file.substr(0,n) + '/_rels' + file.substr(n) + ".rels"; +} + function parse_rels(data, currentFilePath) { if (!data) return data; if (currentFilePath.charAt(0) !== '/') { @@ -2760,28 +2790,13 @@ function parse_rels(data, currentFilePath) { } var rels = {}; var hash = {}; - var resolveRelativePathIntoAbsolute = function (to) { - var toksFrom = currentFilePath.split('/'); - toksFrom.pop(); // folder path - var toksTo = to.split('/'); - var reversed = []; - while (toksTo.length !== 0) { - var tokTo = toksTo.shift(); - if (tokTo === '..') { - toksFrom.pop(); - } else if (tokTo !== '.') { - toksFrom.push(tokTo); - } - } - return toksFrom.join('/'); - }; (data.match(tagregex)||[]).forEach(function(x) { var y = parsexmltag(x); /* 9.3.2.2 OPC_Relationships */ if (y[0] === '= 2) { o.dt = blob.read_shift(2); blob.l -= 2; } switch(o.BIFFVer) { case 0x0600: /* BIFF8 */ case 0x0500: /* BIFF5 */ @@ -3757,6 +3783,7 @@ function parse_BOF(blob, length) { break; default: if(length > 6) throw new Error("Unexpected BIFF Ver " + o.BIFFVer); } + blob.read_shift(length); return o; } @@ -3766,7 +3793,7 @@ function parse_BOF(blob, length) { function parse_InterfaceHdr(blob, length) { if(length === 0) return 0x04b0; var q; - if((q=blob.read_shift(2))!==0x04b0) throw 'InterfaceHdr codePage ' + q; + if((q=blob.read_shift(2))!==0x04b0) throw new Error("InterfaceHdr codePage " + q); return 0x04b0; } @@ -3832,7 +3859,7 @@ function parse_Row(blob, length) { /* 2.4.125 */ function parse_ForceFullCalculation(blob, length) { var header = parse_frtHeader(blob); - if(header.type != 0x08A3) throw "Invalid Future Record " + header.type; + if(header.type != 0x08A3) throw new Error("Invalid Future Record " + header.type); var fullcalc = blob.read_shift(4); return fullcalc !== 0x0; } @@ -3921,9 +3948,9 @@ function parse_MulRk(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var rkrecs = []; while(blob.l < target) rkrecs.push(parse_RkRec(blob)); - if(blob.l !== target) throw "MulRK read error"; + if(blob.l !== target) throw new Error("MulRK read error"); var lastcol = blob.read_shift(2); - if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch"; + if(rkrecs.length != lastcol - col + 1) throw new Error("MulRK length mismatch"); return {r:rw, c:col, C:lastcol, rkrec:rkrecs}; } /* 2.4.174 */ @@ -3932,9 +3959,9 @@ function parse_MulBlank(blob, length) { var rw = blob.read_shift(2), col = blob.read_shift(2); var ixfes = []; while(blob.l < target) ixfes.push(blob.read_shift(2)); - if(blob.l !== target) throw "MulBlank read error"; + if(blob.l !== target) throw new Error("MulBlank read error"); var lastcol = blob.read_shift(2); - if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch"; + if(ixfes.length != lastcol - col + 1) throw new Error("MulBlank length mismatch"); return {r:rw, c:col, C:lastcol, ixfe:ixfes}; } @@ -3998,7 +4025,7 @@ function parse_Guts(blob, length) { var out = [blob.read_shift(2), blob.read_shift(2)]; if(out[0] !== 0) out[0]--; if(out[1] !== 0) out[1]--; - if(out[0] > 7 || out[1] > 7) throw "Bad Gutters: " + out.join("|"); + if(out[0] > 7 || out[1] > 7) throw new Error("Bad Gutters: " + out.join("|")); return out; } @@ -4165,14 +4192,14 @@ try { //var fmla = parse_ObjFmla(blob, s + length - blob.l); for(var i = 1; i < blob.lens.length-1; ++i) { - if(blob.l-s != blob.lens[i]) throw "TxO: bad continue record"; + if(blob.l-s != blob.lens[i]) throw new Error("TxO: bad continue record"); var hdr = blob[blob.l]; var t = parse_XLUnicodeStringNoCch(blob, blob.lens[i+1]-blob.lens[i]-1); texts += t; if(texts.length >= (hdr ? cchText : 2*cchText)) break; } if(texts.length !== cchText && texts.length !== cchText*2) { - throw "cchText: " + cchText + " != " + texts.length; + throw new Error("cchText: " + cchText + " != " + texts.length); } blob.l = s + length; @@ -4180,9 +4207,9 @@ try { // var rgTxoRuns = []; // for(var j = 0; j != cbRuns/8-1; ++j) blob.l += 8; // var cchText2 = blob.read_shift(2); -// if(cchText2 !== cchText) throw "TxOLastRun mismatch: " + cchText2 + " " + cchText; +// if(cchText2 !== cchText) throw new Error("TxOLastRun mismatch: " + cchText2 + " " + cchText); // blob.l += 6; -// if(s + length != blob.l) throw "TxO " + (s + length) + ", at " + blob.l; +// if(s + length != blob.l) throw new Error("TxO " + (s + length) + ", at " + blob.l); return { t: texts }; } catch(e) { blob.l = s + length; return { t: texts }; } } @@ -4252,6 +4279,15 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.261 */ +function parse_ShtProps(blob, length, opts) { + var def = {area:false}; + if(opts.biff != 5) { blob.l += length; return def; } + var d = blob.read_shift(1); blob.l += 3; + if((d & 0x10)) def.area = true; + return def; +} + var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -4544,7 +4580,6 @@ var parse_Surf = parsenoop; var parse_RadarArea = parsenoop; var parse_AxisParent = parsenoop; var parse_LegendException = parsenoop; -var parse_ShtProps = parsenoop; var parse_SerToCrt = parsenoop; var parse_AxesUsed = parsenoop; var parse_SBaseRef = parsenoop; @@ -5444,7 +5479,7 @@ function parse_cellXfs(t, styles, opts) { /* 18.2.10 extLst CT_ExtensionList ? */ case '': break; case ']*r:id="([^"]*)"/)||["",""])[1]; + + return rels['!id'][id].Target; +} function parse_comments(zip, dirComments, sheets, sheetRels, opts) { for(var i = 0; i != dirComments.length; ++i) { @@ -8500,7 +8553,10 @@ function csf_to_ods_formula(f) { var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options -RELS.WS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"; +RELS.WS = [ + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", + "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet" +]; function get_sst_id(sst, str) { for(var i = 0, len = sst.length; i < len; ++i) if(sst[i].t === str) { sst.Count ++; return i; } @@ -9526,6 +9582,94 @@ function write_ws_bin(idx, opts, wb) { write_record(ba, "BrtEndSheet"); return ba.end(); } +function parse_numCache(data) { + var col = []; + + /* 21.2.2.150 pt CT_NumVal */ + (data.match(/(.*?)<\/c:pt>/mg)||[]).forEach(function(pt) { + var q = pt.match(/(.*)<\/c:v><\/c:pt>/); + if(!q) return; + col[+q[1]] = +q[2]; + }); + + /* 21.2.2.71 formatCode CT_Xstring */ + var nf = unescapexml((data.match(/(.*?)<\/c:formatCode>/) || ["","General"])[1]); + + return [col, nf]; +} + +/* 21.2 DrawingML - Charts */ +function parse_chart(data, name, opts, rels, wb, csheet) { + var cs = ((csheet || {"!type":"chart"})); + if(!data) return csheet; + /* 21.2.2.27 chart CT_Chart */ + + var C = 0, R = 0, col = "A"; + var refguess = {s: {r:2000000, c:2000000}, e: {r:0, c:0} }; + + /* 21.2.2.120 numCache CT_NumData */ + (data.match(/.*?<\/c:numCache>/gm)||[]).forEach(function(nc) { + var cache = parse_numCache(nc); + refguess.s.r = refguess.s.c = 0; + refguess.e.c = C; + col = encode_col(C); + cache[0].forEach(function(n,i) { + cs[col + encode_row(i)] = {t:'n', v:n, z:cache[1] }; + R = i; + }); + if(refguess.e.r < R) refguess.e.r = R; + ++C; + }); + if(C > 0) cs["!ref"] = encode_range(refguess); + return cs; +} +RELS.CS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"; + +/* 18.3 Worksheets also covers Chartsheets */ +function parse_cs_xml(data, opts, rels, wb, themes, styles) { + if(!data) return data; + /* 18.3.1.12 chartsheet CT_ChartSheet */ + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var m; + + /* 18.3.1.36 drawing CT_Drawing */ + if((m = data.match(/drawing r:id="(.*?)"/))) s['!rel'] = m[1]; + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} + +/* [MS-XLSB] 2.1.7.7 Chart Sheet */ +function parse_cs_bin(data, opts, rels, wb, themes, styles) { + if(!data) return data; + if(!rels) rels = {'!id':{}}; + var s = {'!type':"chart", '!chart':null, '!rel':""}; + var pass = false; + recordhopper(data, function cs_parse(val, Record) { + switch(Record.n) { + + case 'BrtDrawing': s['!rel'] = val; break; + + case 'BrtBeginSheet': break; + case 'BrtCsProp': break; // TODO + case 'BrtBeginCsViews': break; // TODO + case 'BrtBeginCsView': break; // TODO + case 'BrtEndCsView': break; // TODO + case 'BrtEndCsViews': break; // TODO + case 'BrtCsProtection': break; // TODO + case 'BrtMargins': break; // TODO + case 'BrtCsPageSetup': break; // TODO + case 'BrtEndSheet': break; // TODO + case 'BrtBeginHeaderFooter': break; // TODO + case 'BrtEndHeaderFooter': break; // TODO + default: if(!pass || opts.WTF) throw new Error("Unexpected record " + Record.n); + } + }, opts); + + if(rels['!id'][s['!rel']]) s['!chart'] = rels['!id'][s['!rel']]; + return s; +} /* 18.2.28 (CT_WorkbookProtection) Defaults */ var WBPropsDef = [ ['allowRefreshQuery', '0'], @@ -10017,6 +10161,11 @@ function parse_ws(data, name, opts, rels, wb, themes, styles) { return parse_ws_xml((data), opts, rels, wb, themes, styles); } +function parse_cs(data, name, opts, rels, wb, themes, styles) { + if(name.slice(-4)===".bin") return parse_cs_bin((data), opts, rels, wb, themes, styles); + return parse_cs_xml((data), opts, rels, wb, themes, styles); +} + function parse_sty(data, name, themes, opts) { if(name.slice(-4)===".bin") return parse_sty_bin((data), themes, opts); return parse_sty_xml((data), themes, opts); @@ -11196,7 +11345,6 @@ function parse_workbook(blob, options) { case 'EOF': { if(--file_depth) break; if(range.e) { - out["!range"] = range; if(range.e.r > 0 && range.e.c > 0) { range.e.r--; range.e.c--; out["!ref"] = encode_range(range); @@ -11231,6 +11379,7 @@ function parse_workbook(blob, options) { opts.snames.push(cur_sheet); } else cur_sheet = (Directory[s] || {name:""}).name; + if(val.dt == 0x20) out["!type"] = "chart"; mergecells = []; objects = []; array_formulae = []; opts.arrayf = array_formulae; @@ -11240,6 +11389,7 @@ function parse_workbook(blob, options) { } break; case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { + if(out["!type"] == "chart" && out[encode_cell({c:val.c, r:val.r})]) ++val.c; temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); @@ -12166,7 +12316,7 @@ var XLSBRecordEnum = { 0x021E: { n:"BrtBeginECTwFldInfo", f:parsenoop }, 0x0224: { n:"BrtFileSharing", f:parsenoop }, 0x0225: { n:"BrtOleSize", f:parsenoop }, -0x0226: { n:"BrtDrawing", f:parsenoop }, +0x0226: { n:"BrtDrawing", f:parse_RelID }, 0x0227: { n:"BrtLegacyDrawing", f:parsenoop }, 0x0228: { n:"BrtLegacyDrawingHF", f:parsenoop }, 0x0229: { n:"BrtWebOpt", f:parsenoop }, @@ -13642,16 +13792,38 @@ var fix_write_opts = fix_opts_func([ ]); function safe_parse_wbrels(wbrels, sheets) { if(!wbrels) return 0; + function get_type(n) { + if(RELS.WS.indexOf(n) > -1) return "sheet"; + if(RELS.CS && n == RELS.CS) return "chart"; + if(RELS.DS && n == RELS.DS) return "dialog"; + if(RELS.MS && n == RELS.MS) return "macro"; + if(!n || !n.length) return "sheet"; + return n; + } try { - wbrels = sheets.map(function pwbr(w) { return [w.name, wbrels['!id'][w.id].Target]; }); + wbrels = sheets.map(function pwbr(w) { if(!w.id) w.id = w.strRelID; return [w.name, wbrels['!id'][w.id].Target, get_type(wbrels['!id'][w.id].Type)]; }); } catch(e) { return null; } return !wbrels || wbrels.length === 0 ? null : wbrels; } -function safe_parse_ws(zip, path, relsPath, sheet, sheetRels, sheets, opts, wb, themes, styles) { +function safe_parse_sheet(zip, path, relsPath, sheet, sheetRels, sheets, stype, opts, wb, themes, styles) { try { sheetRels[sheet]=parse_rels(getzipstr(zip, relsPath, true), path); - sheets[sheet]=parse_ws(getzipdata(zip, path),path,opts,sheetRels[sheet], wb, themes, styles); + var data = getzipdata(zip, path); + switch(stype) { + case 'sheet': sheets[sheet]=parse_ws(data, path, opts,sheetRels[sheet], wb, themes, styles); break; + case 'chart': + var cs = parse_cs(data, path, opts,sheetRels[sheet], wb, themes, styles); + sheets[sheet] = cs; + if(!cs || !cs['!chart']) break; + var dfile = resolve_path(cs['!chart'].Target, path); + var drelsp = get_rels_path(dfile); + var draw = parse_drawing(getzipstr(zip, dfile, true), parse_rels(getzipstr(zip,drelsp,true), dfile)); + var chartp = resolve_path(draw, dfile); + var crelsp = get_rels_path(chartp); + cs = parse_chart(getzipstr(zip, chartp, true), chartp, opts, parse_rels(getzipstr(zip, crelsp,true), chartp), wb, cs); + break; + } } catch(e) { if(opts.WTF) throw e; } } @@ -13718,8 +13890,8 @@ function parse_zip(zip, opts) { var out = ({}); if(opts.bookSheets || opts.bookProps) { - if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; - else if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + if(wb.Sheets) sheets = wb.Sheets.map(function pluck(x){ return x.name; }); + else if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; if(opts.bookProps) { out.Props = props; out.Custprops = custprops; } if(opts.bookSheets && typeof sheets !== 'undefined') out.SheetNames = sheets; if(opts.bookSheets ? out.SheetNames : opts.bookProps) return out; @@ -13748,13 +13920,16 @@ function parse_zip(zip, opts) { /* Numbers iOS hack */ var nmode = (getzipdata(zip,"xl/worksheets/sheet.xml",true))?1:0; for(i = 0; i != props.Worksheets; ++i) { - if(wbrels && wbrels[i]) path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); - else { + var stype = "sheet"; + if(wbrels && wbrels[i]) { + path = 'xl/' + (wbrels[i][1]).replace(/[\/]?xl\//, ""); + stype = wbrels[i][2]; + } else { path = 'xl/worksheets/sheet'+(i+1-nmode)+"." + wbext; path = path.replace(/sheet0\./,"sheet."); } relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); - safe_parse_ws(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, opts, wb, themes, styles); + safe_parse_sheet(zip, path, relsPath, props.SheetNames[i], sheetRels, sheets, stype, opts, wb, themes, styles); } if(dir.comments) parse_comments(zip, dir.comments, sheets, sheetRels, opts); @@ -13847,7 +14022,7 @@ f = "docProps/app.xml"; f = "xl/worksheets/sheet" + rId + "." + wbext; zip.file(f, write_ws(rId-1, f, opts, wb)); ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS); + add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); } if(opts.Strings != null && opts.Strings.length > 0) {