diff --git a/.eslintrc b/.eslintrc index 20c365e..ff5e970 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,7 @@ "curly": 0, "comma-style": [ 2, "last" ], "no-trailing-spaces": 2, + "semi": [ 2, "always" ], "comma-dangle": [ 2, "never" ] } } diff --git a/README.md b/README.md index 18ee067..b9248a0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ enhancements and additional features by request. * [Document Features](#document-features) + [Formulae](#formulae) + [Column Properties](#column-properties) + + [Row Properties](#row-properties) + [Hyperlinks](#hyperlinks) + [Cell Comments](#cell-comments) + [Sheet Visibility](#sheet-visibility) @@ -97,6 +98,7 @@ enhancements and additional features by request. * [Tested Environments](#tested-environments) * [Test Files](#test-files) - [Contributing](#contributing) + * [Tests](#tests) * [OSX/Linux](#osxlinux) * [Windows](#windows) - [License](#license) @@ -630,33 +632,37 @@ In addition to the base sheet keys, worksheets also add: parsed, the column objects store the pixel width in the `wpx` field, character width in the `wch` field, and the maximum digit width in the `MDW` field. +- `ws['!rows']`: array of row properties objects as explained later in the docs. + Each row object encodes properties including row height and visibility. + - `ws['!merges']`: array of range objects corresponding to the merged cells in the worksheet. Plaintext utilities are unaware of merge cells. CSV export 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. -- `ws['protect']`: object of write sheet protection properties. The `password` +- `ws['!protect']`: object of write sheet protection properties. The `password` key specifies the password for formats that support password-protected sheets (XLSX/XLSB/XLS). The writer uses the XOR obfuscation method. The following - keys control the sheet protection (same as ECMA-376 18.3.1.85): + keys control the sheet protection -- set to `false` to enable a feature when + sheet is locked or set to `true` to disable a feature: -| key | functionality disabled if value is true | -|:----------------------|:-----------------------------------------------------| -| `selectLockedCells` | Select locked cells | -| `selectUnlockedCells` | Select unlocked cells | -| `formatCells` | Format cells | -| `formatColumns` | Format columns | -| `formatRows` | Format rows | -| `insertColumns` | Insert columns | -| `insertRows` | Insert rows | -| `insertHyperlinks` | Insert hyperlinks | -| `deleteColumns` | Delete columns | -| `deleteRows` | Delete rows | -| `sort` | Sort | -| `autoFilter` | Filter | -| `pivotTables` | Use PivotTable reports | -| `objects` | Edit objects | -| `scenarios` | Edit scenarios | +| key | feature (true=disabled / false=enabled) | default | +|:----------------------|:----------------------------------------|:-----------| +| `selectLockedCells` | Select locked cells | enabled | +| `selectUnlockedCells` | Select unlocked cells | enabled | +| `formatCells` | Format cells | disabled | +| `formatColumns` | Format columns | disabled | +| `formatRows` | Format rows | disabled | +| `insertColumns` | Insert columns | disabled | +| `insertRows` | Insert rows | disabled | +| `insertHyperlinks` | Insert hyperlinks | disabled | +| `deleteColumns` | Delete columns | disabled | +| `deleteRows` | Delete rows | disabled | +| `sort` | Sort | disabled | +| `autoFilter` | Filter | disabled | +| `pivotTables` | Use PivotTable reports | disabled | +| `objects` | Edit objects | enabled | +| `scenarios` | Edit scenarios | enabled | - `ws['!autofilter']`: AutoFilter object following the schema: @@ -835,6 +841,7 @@ Since Excel prohibits named cells from colliding with names of A1 or RC style cell references, a (not-so-simple) regex conversion is possible. BIFF Parsed formulae have to be explicitly unwound. OpenFormula formulae can be converted with regexes for the most part. + #### Column Properties Excel internally stores column widths in a nebulous "Max Digit Width" form. The @@ -853,10 +860,11 @@ objects which have the following properties: ```typescript type ColInfo = { - MDW?:number; // Excel's "Max Digit Width" unit, always integral - width:number; // width in Excel's "Max Digit Width", width*256 is integral - wpx?:number; // width in screen pixels - wch?:number; // intermediate character calculation + MDW?:number; // Excel's "Max Digit Width" unit, always integral + width:number; // width in Excel's "Max Digit Width", width*256 is integral + wpx?:number; // width in screen pixels + wch?:number; // intermediate character calculation + hidden:?boolean; // if true, the column is hidden }; ``` @@ -867,6 +875,29 @@ follow the priority order: 2) use `wpx` pixel width if available 3) use `wch` character count if available +#### Row Properties + +Excel internally stores row heights in points. The default resolution is 72 DPI +or 96 PPI, so the pixel and point size should agree. For different resolutions +they may not agree, so the library separates the concepts. + +The `!rows` array in each worksheet, if present, is a collection of `RowInfo` +objects which have the following properties: + +```typescript +type RowInfo = { + hpx?:number; // height in screen pixels + hpt?:number; // height in points + hidden:?boolean; // if true, the row is hidden +}; +``` + +Even though all of the information is made available, writers are expected to +follow the priority order: + +1) use `hpx` pixel height if available +2) use `hpt` point height if available + #### Hyperlinks Hyperlinks are stored in the `l` key of cell objects. The `Target` field of the @@ -1520,6 +1551,25 @@ Running `make init` will refresh the `test_files` submodule and get the files. Due to the precarious nature of the Open Specifications Promise, it is very important to ensure code is cleanroom. Consult CONTRIBUTING.md +### Tests + +The `test_misc` target (`make test_misc` on Linux/OSX / `make misc` on Windows) +runs the targeted feature tests. It should take 5-10 seconds to perform feature +tests without testing against the entire test battery. New features should be +accompanied with tests for the relevant file formats and features. + +For tests involving the read side, an appropriate feature test would involve +reading an existing file and checking the resulting workbook object. If a +parameter is involved, files should be read with different values for the param +to verify that the feature is working as expected. + +For tests involving a new write feature which can already be parsed, appropriate +feature tests would involve writing a workbook with the feature and then opening +and verifying that the feature is preserved. + +For tests involving a new write feature without an existing read ability, please +add a feature test to the kitchen sink `tests/write.js`. + ### OSX/Linux The xlsx.js file is constructed from the files in the `bits` subdirectory. The diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js index d0b2d64..cddd7e0 100644 --- a/bits/39_xlsbiff.js +++ b/bits/39_xlsbiff.js @@ -210,14 +210,19 @@ function parse_ExtSST(blob, length) { } -/* 2.4.221 TODO*/ +/* 2.4.221 TODO: check BIFF2-4 */ function parse_Row(blob, length) { - var rw = blob.read_shift(2), col = blob.read_shift(2), Col = blob.read_shift(2), rht = blob.read_shift(2); - blob.read_shift(4); // reserved(2), unused(2) + var z = ({}/*:any*/); + z.r = blob.read_shift(2); + z.c = blob.read_shift(2); + z.cnt = blob.read_shift(2) - z.c; + var miyRw = blob.read_shift(2); + blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags - blob.read_shift(1); // reserved - blob.read_shift(2); //ixfe, other flags - return {r:rw, c:col, cnt:Col-col}; + blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x20) z.hidden = true; + if(flags & 0x40) z.hpt = miyRw / 20; + return z; } diff --git a/bits/40_harb.js b/bits/40_harb.js index bb0e6ad..d6e3d9c 100644 --- a/bits/40_harb.js +++ b/bits/40_harb.js @@ -206,19 +206,19 @@ var SYLK = (function() { var records = str.split(/[\n\r]+/), R = -1, C = -1, ri = 0, rj = 0, arr = []; var formats = []; var next_cell_format = null; + var sht = {}, rowinfo = [], colinfo = [], cw = []; + var Mval = 0, j; for (; ri !== records.length; ++ri) { + Mval = 0; var record = records[ri].trim().split(";"); var RT = record[0], val; - if(RT === 'P') for(rj=1; rj 0) { rowinfo[R].hpt = Mval; rowinfo[R].hpx = pt2px(Mval); } + else if(Mval == 0) rowinfo[R].hidden = true; + } break; + default: break; } } + if(rowinfo.length > 0) sht['!rows'] = rowinfo; + if(colinfo.length > 0) sht['!cols'] = colinfo; + arr[arr.length] = sht; return arr; } - function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(sylk_to_aoa(str, opts), opts); } + function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { + var aoa = sylk_to_aoa(str, opts); + var ws = aoa.pop(); + var o = aoa_to_sheet(aoa, opts); + keys(ws).forEach(function(k) { o[k] = ws[k]; }); + return o; + } function sylk_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(sylk_to_sheet(str, opts), opts); } @@ -257,11 +282,40 @@ var SYLK = (function() { return o; } + function write_ws_cols_sylk(out, cols) { + cols.forEach(function(col, i) { + var rec = "F;W" + (i+1) + " " + (i+1) + " "; + if(col.hidden) rec += "0"; + else { + if(typeof col.width == 'number') col.wpx = width2px(col.width); + if(typeof col.wpx == 'number') col.wch = px2char(col.wpx); + if(typeof col.wch == 'number') rec += Math.round(col.wch); + } + if(rec.charAt(rec.length - 1) != " ") out.push(rec); + }); + } + + function write_ws_rows_sylk(out, rows) { + rows.forEach(function(row, i) { + var rec = "F;"; + if(row.hidden) rec += "M0;"; + else if(row.hpt) rec += "M" + 20 * row.hpt + ";"; + else if(row.hpx) rec += "M" + 20 * px2pt(row.hpx) + ";"; + if(rec.length > 2) out.push(rec + "R" + (i+1)); + }); + } + function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/)/*:string*/ { var preamble/*:Array*/ = ["ID;PWXL;N;E"], o/*:Array*/ = []; - preamble.push("P;PGeneral"); var r = decode_range(ws['!ref']), cell/*:Cell*/; var dense = Array.isArray(ws); + var RS = "\r\n"; + + preamble.push("P;PGeneral"); + preamble.push("F;P0;DG0G8;M255"); + if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']); + if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']); + for(var R = r.s.r; R <= r.e.r; ++R) { for(var C = r.s.c; C <= r.e.c; ++C) { var coord = encode_cell({r:R,c:C}); @@ -270,8 +324,6 @@ var SYLK = (function() { o.push(write_ws_cell_sylk(cell, ws, R, C, opts)); } } - preamble.push("F;P0;DG0G8;M255"); - var RS = "\r\n"; return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS; } diff --git a/bits/45_styutils.js b/bits/45_styutils.js index 674ff57..5923884 100644 --- a/bits/45_styutils.js +++ b/bits/45_styutils.js @@ -85,13 +85,17 @@ function process_col(coll/*:ColInfo*/) { coll.wch = px2char(coll.wpx); coll.width = char2width(coll.wch); coll.MDW = MDW; + } else if(typeof coll.wch == 'number') { + coll.width = char2width(coll.wch); + coll.wpx = width2px(coll.width); + coll.MDW = MDW; } if(coll.customWidth) delete coll.customWidth; } -var DEF_DPI = 96, DPI = DEF_DPI; -function px2pt(px) { return px * 72 / DPI; } -function pt2px(pt) { return pt * DPI / 72; } +var DEF_PPI = 96, PPI = DEF_PPI; +function px2pt(px) { return px * 96 / PPI; } +function pt2px(pt) { return pt * PPI / 96; } /* [MS-EXSPXML3] 2.4.54 ST_enmPattern */ var XLMLPatternTypeMap = { diff --git a/bits/66_wscommon.js b/bits/66_wscommon.js index b35674b..d7ce1eb 100644 --- a/bits/66_wscommon.js +++ b/bits/66_wscommon.js @@ -14,13 +14,14 @@ function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ { function col_obj_w(C/*:number*/, col) { var p = ({min:C+1,max:C+1}/*:any*/); /* wch (chars), wpx (pixels) */ - var width = -1; + var wch = -1; if(col.MDW) MDW = col.MDW; if(col.width != null) p.customWidth = 1; - else if(col.wpx != null) width = px2char(col.wpx); - else if(col.wch != null) width = col.wch; - if(width > -1) { p.width = char2width(width); p.customWidth = 1; } - else p.width = col.width; + else if(col.wpx != null) wch = px2char(col.wpx); + else if(col.wch != null) wch = col.wch; + if(wch > -1) { p.width = char2width(wch); p.customWidth = 1; } + else if(col.width != null) p.width = col.width; + if(col.hidden) p.hidden = true; return p; } diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 9038e83..d7f0bd0 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -152,6 +152,7 @@ function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { var coll = parsexmltag(cols[coli], true); + if(coll.hidden) coll.hidden = parsexmlbool(coll.hidden); var colm=parseInt(coll.min, 10)-1, colM=parseInt(coll.max,10)-1; delete coll.min; delete coll.max; coll.width = +coll.width; if(!seencol && coll.width) { seencol = true; find_mdw_colw(coll.width); } @@ -178,6 +179,12 @@ function write_ws_xml_autofilter(data)/*:string*/ { return writextag("autoFilter", null, {ref:data.ref}); } +/* 18.3.1.88 sheetViews CT_SheetViews */ +/* 18.3.1.87 sheetView CT_SheetView */ +function write_ws_xml_sheetviews(ws, opts, idx, wb)/*:string*/ { + return writextag("sheetViews", writextag("sheetView", null, {workbookViewId:"0"}), {}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -229,13 +236,14 @@ var parse_ws_xml_data = (function parse_ws_xml_data_factory() { var match_v = matchtag("v"), match_f = matchtag("f"); return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { - var ri = 0, x = "", cells = [], cref = [], idx = 0, i=0, cc=0, d="", p/*:any*/; + var ri = 0, x = "", cells = [], cref = [], idx=0, i=0, cc=0, d="", p/*:any*/; var tag, tagr = 0, tagc = 0; var sstr, ftag; var fmtid = 0, fillid = 0, do_format = Array.isArray(styles.CellXf), cf; var arrayf = []; var sharedf = []; var dense = Array.isArray(s); + var rows = [], rowobj = {}, rowrite = false; for(var marr = sdata.split(rowregex), mt = 0, marrlen = marr.length; mt != marrlen; ++mt) { x = marr[mt].trim(); var xlen = x.length; @@ -249,6 +257,13 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { if(guess.s.r > tagr - 1) guess.s.r = tagr - 1; if(guess.e.r < tagr - 1) guess.e.r = tagr - 1; + if(opts && opts.cellStyles) { + rowobj = {}; rowrite = false; + if(tag.ht) { rowrite = true; rowobj.hpt = parseFloat(tag.ht); rowobj.hpx = pt2px(rowobj.hpt); } + if(tag.hidden == "1") { rowrite = true; rowobj.hidden = true; } + if(rowrite) rows[tagr-1] = rowobj; + } + /* 18.3.1.4 c CT_Cell */ cells = x.substr(ri).split(cellregex); for(ri = 0; ri != cells.length; ++ri) { @@ -357,6 +372,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } else s[tag.r] = p; } } + if(rows.length > 0) s['!rows'] = rows; }; })(); function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/, rels)/*:string*/ { @@ -407,7 +423,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { o[o.length] = (writextag('dimension', null, {'ref': ref})); - /* sheetViews */ + o[o.length] = write_ws_xml_sheetviews(ws, opts, idx, wb); /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); @@ -457,7 +473,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { delete ws['!links']; /* printOptions */ - if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']) + if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']); /* pageSetup */ var hfidx = o.length; diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index 3b30a40..6f4a02a 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -1,20 +1,38 @@ /* [MS-XLSB] 2.4.718 BrtRowHdr */ function parse_BrtRowHdr(data, length) { - var z = ([]/*:any*/); + var z = ({}/*:any*/); + var tgt = data.l + length; z.r = data.read_shift(4); - data.l += length-4; + data.l += 4; // TODO: ixfe + var miyRw = data.read_shift(2); + data.l += 1; // TODO: top/bot padding + var flags = data.read_shift(1); + data.l = tgt; + if(flags & 0x10) z.hidden = true; + if(flags & 0x20) z.hpt = miyRw / 20; return z; } function write_BrtRowHdr(R/*:number*/, range, ws) { var o = new_buf(17+8*16); + var row = (ws['!rows']||[])[R]||{}; o.write_shift(4, R); - /* TODO: flags styles */ - o.write_shift(4, 0); - o.write_shift(2, 0x0140); - o.write_shift(2, 0); - o.write_shift(1, 0); + o.write_shift(4, 0); /* TODO: ixfe */ + + var miyRw = 0x0140; + if(row.hpx) miyRw = px2pt(row.hpx) * 20; + else if(row.hpt) miyRw = row.hpt * 20; + o.write_shift(2, miyRw); + + o.write_shift(1, 0); /* top/bot padding */ + + var flags = 0x0; + if(row.hidden) flags |= 0x10; + if(row.hpx || row.hpt) flags |= 0x20; + o.write_shift(1, flags); + + o.write_shift(1, 0); /* phonetic guide */ /* [MS-XLSB] 2.5.8 BrtColSpan explains the mechanism */ var ncolspan = 0, lcs = o.l; @@ -282,9 +300,12 @@ function write_BrtColInfo(C/*:number*/, col, o) { var p = col_obj_w(C, col); o.write_shift(-4, C); o.write_shift(-4, C); - o.write_shift(4, p.width * 256); + o.write_shift(4, (p.width || 10) * 256); o.write_shift(4, 0/*ixfe*/); // style - o.write_shift(1, 2); // bit flag + var flags = 0; + if(col.hidden) flags |= 0x01; + if(typeof p.width == 'number') flags |= 0x02; + o.write_shift(1, flags); // bit flag o.write_shift(1, 0); // bit flag return o; } @@ -312,6 +333,24 @@ function write_BrtMargins(margins, o) { return o; } +/* [MS-XLSB] 2.4.292 BrtBeginWsView */ +function write_BrtBeginWsView(ws, o) { + if(o == null) o = new_buf(30); + o.write_shift(2, 924); // bit flag + o.write_shift(4, 0); + o.write_shift(4, 0); // view first row + o.write_shift(4, 0); // view first col + o.write_shift(1, 0); // gridline color ICV + o.write_shift(1, 0); + o.write_shift(2, 0); + o.write_shift(2, 100); // zoom scale + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(4, 0); // workbook view id + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -334,9 +373,8 @@ function write_BrtSheetProtection(sp, o) { ["pivotTables", true], // fPivotTables ["selectUnlockedCells", false] // fSelUnlockedCells ].forEach(function(n) { - o.write_shift(4, 1); - if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); - else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + if(n[1]) o.write_shift(4, sp[n[0]] != null && !sp[n[0]] ? 1 : 0); + else o.write_shift(4, sp[n[0]] != null && sp[n[0]] ? 0 : 1); }); return o; } @@ -383,6 +421,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(opts.sheetRows && opts.sheetRows <= row.r) end=true; rr = encode_row(R = row.r); opts['!row'] = row.r; + if(val.hidden || val.hpt) { + if(val.hpt) val.hpx = pt2px(val.hpt); + rowinfo[val.r] = val; + } break; case 0x0002: /* 'BrtCellRk' */ @@ -480,7 +522,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { case 0x003C: /* 'BrtColInfo' */ if(!opts.cellStyles) break; while(val.e >= val.s) { - colinfo[val.e--] = { width: val.w/256 }; + colinfo[val.e--] = { width: val.w/256, hidden: !!(val.flags & 0x01) }; if(!seencol) { seencol = true; find_mdw_colw(val.w/256); } process_col(colinfo[val.e+1]); } @@ -696,6 +738,21 @@ function write_AUTOFILTER(ba, ws) { write_record(ba, "BrtEndAFilter"); } +function write_WSVIEWS2(ba, ws) { + write_record(ba, "BrtBeginWsViews"); + { /* 1*WSVIEW2 */ + /* [ACUID] */ + write_record(ba, "BrtBeginWsView", write_BrtBeginWsView(ws)); + /* [BrtPane] */ + /* *4BrtSel */ + /* *4SXSELECT */ + /* *FRT */ + write_record(ba, "BrtEndWsView"); + } + /* *FRT */ + write_record(ba, "BrtEndWsViews"); +} + function write_SHEETPROTECT(ba, ws) { if(!ws['!protect']) return; /* [BrtSheetProtectionIso] */ @@ -712,7 +769,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); - /* [WSVIEWS2] */ + write_WSVIEWS2(ba, ws); /* [WSFMTINFO] */ write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 9e90060..3ec8e8c 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -189,7 +189,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { var comments = [], comment = {}; var cstys = [], csty, seencol = false; var arrayf = []; - var rowinfo = []; + var rowinfo = [], rowobj = {}; var Workbook = { Sheets:[] }, wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); @@ -254,6 +254,12 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } else { row = xlml_parsexmltag(Rn[0]); if(row.Index) r = +row.Index - 1; + rowobj = {}; + if(row.AutoFitHeight == "0") { + rowobj.hpx = parseInt(row.Height, 10); rowobj.hpt = px2pt(rowobj.hpx); + rowinfo[r] = rowobj; + } + if(row.Hidden == "1") { rowobj.hidden = true; rowinfo[r] = rowobj; } } break; case 'Worksheet': /* TODO: read range from FullRows/FullColumns */ @@ -304,9 +310,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Column': if(state[state.length-1][0] !== 'Table') break; csty = xlml_parsexmltag(Rn[0]); - csty.wpx = parseInt(csty.Width, 10); + if(csty.Hidden) { csty.hidden = true; delete csty.Hidden; } + if(csty.Width) csty.wpx = parseInt(csty.Width, 10); if(!seencol && csty.wpx > 10) { - seencol = true; find_mdw_wpx(csty.wpx); + seencol = true; MDW = DEF_MDW; //find_mdw_wpx(csty.wpx); for(var _col = 0; _col < cstys.length; ++_col) if(cstys[_col]) process_col(cstys[_col]); } if(seencol) process_col(csty); @@ -443,7 +450,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Color': break; case 'Index': break; case 'RGB': break; - case 'PixelsPerInch': break; + case 'PixelsPerInch': break; // TODO: set PPI case 'TargetScreenSize': break; case 'ReadOnlyRecommended': break; default: seen = false; @@ -995,6 +1002,15 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ return writextag("Cell", m, attr); } +function write_ws_xlml_row(R/*:number*/, row)/*:string*/ { + var o = ''; +} /* TODO */ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/)/*:string*/ { if(!ws['!ref']) return ""; @@ -1002,12 +1018,17 @@ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbo var marr = ws['!merges'] || [], mi = 0; var o = []; if(ws['!cols']) ws['!cols'].forEach(function(n, i) { + process_col(n); + var w = !!n.width; var p = col_obj_w(i, n); - o.push(writextag("Column",null, {"ss:Index":i+1, "ss:Width":width2px(p.width)})); + var k = {"ss:Index":i+1}; + if(w) k['ss:Width'] = width2px(p.width); + if(n.hidden) k['ss:Hidden']="1"; + o.push(writextag("Column",null,k)); }); var dense = Array.isArray(ws); for(var R = range.s.r; R <= range.e.r; ++R) { - var row = ['']; + var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])]; for(var C = range.s.c; C <= range.e.c; ++C) { var skip = false; for(mi = 0; mi != marr.length; ++mi) { diff --git a/bits/76_xls.js b/bits/76_xls.js index a8318a0..ffcfc2d 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -502,7 +502,14 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { process_col(colinfo[val.e+1]); } } break; - case 'Row': break; // TODO + case 'Row': { + var rowobj = {}; + if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } + if(val.hpt) { + rowinfo[val.r] = rowobj; + rowobj.hpt = val.hpt; rowobj.hpx = pt2px(val.hpt); + } + } break; case 'LeftMargin': case 'RightMargin': diff --git a/docbits/54_shobject.md b/docbits/54_shobject.md index 4f82199..b55d0db 100644 --- a/docbits/54_shobject.md +++ b/docbits/54_shobject.md @@ -8,33 +8,37 @@ In addition to the base sheet keys, worksheets also add: parsed, the column objects store the pixel width in the `wpx` field, character width in the `wch` field, and the maximum digit width in the `MDW` field. +- `ws['!rows']`: array of row properties objects as explained later in the docs. + Each row object encodes properties including row height and visibility. + - `ws['!merges']`: array of range objects corresponding to the merged cells in the worksheet. Plaintext utilities are unaware of merge cells. CSV export 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. -- `ws['protect']`: object of write sheet protection properties. The `password` +- `ws['!protect']`: object of write sheet protection properties. The `password` key specifies the password for formats that support password-protected sheets (XLSX/XLSB/XLS). The writer uses the XOR obfuscation method. The following - keys control the sheet protection (same as ECMA-376 18.3.1.85): + keys control the sheet protection -- set to `false` to enable a feature when + sheet is locked or set to `true` to disable a feature: -| key | functionality disabled if value is true | -|:----------------------|:-----------------------------------------------------| -| `selectLockedCells` | Select locked cells | -| `selectUnlockedCells` | Select unlocked cells | -| `formatCells` | Format cells | -| `formatColumns` | Format columns | -| `formatRows` | Format rows | -| `insertColumns` | Insert columns | -| `insertRows` | Insert rows | -| `insertHyperlinks` | Insert hyperlinks | -| `deleteColumns` | Delete columns | -| `deleteRows` | Delete rows | -| `sort` | Sort | -| `autoFilter` | Filter | -| `pivotTables` | Use PivotTable reports | -| `objects` | Edit objects | -| `scenarios` | Edit scenarios | +| key | feature (true=disabled / false=enabled) | default | +|:----------------------|:----------------------------------------|:-----------| +| `selectLockedCells` | Select locked cells | enabled | +| `selectUnlockedCells` | Select unlocked cells | enabled | +| `formatCells` | Format cells | disabled | +| `formatColumns` | Format columns | disabled | +| `formatRows` | Format rows | disabled | +| `insertColumns` | Insert columns | disabled | +| `insertRows` | Insert rows | disabled | +| `insertHyperlinks` | Insert hyperlinks | disabled | +| `deleteColumns` | Delete columns | disabled | +| `deleteRows` | Delete rows | disabled | +| `sort` | Sort | disabled | +| `autoFilter` | Filter | disabled | +| `pivotTables` | Use PivotTable reports | disabled | +| `objects` | Edit objects | enabled | +| `scenarios` | Edit scenarios | enabled | - `ws['!autofilter']`: AutoFilter object following the schema: diff --git a/docbits/61_formulae.md b/docbits/61_formulae.md index 35df471..a5c98ca 100644 --- a/docbits/61_formulae.md +++ b/docbits/61_formulae.md @@ -77,3 +77,4 @@ Since Excel prohibits named cells from colliding with names of A1 or RC style cell references, a (not-so-simple) regex conversion is possible. BIFF Parsed formulae have to be explicitly unwound. OpenFormula formulae can be converted with regexes for the most part. + diff --git a/docbits/62_columns.md b/docbits/62_colrow.md similarity index 51% rename from docbits/62_columns.md rename to docbits/62_colrow.md index adce380..1ac70f4 100644 --- a/docbits/62_columns.md +++ b/docbits/62_colrow.md @@ -16,10 +16,11 @@ objects which have the following properties: ```typescript type ColInfo = { - MDW?:number; // Excel's "Max Digit Width" unit, always integral - width:number; // width in Excel's "Max Digit Width", width*256 is integral - wpx?:number; // width in screen pixels - wch?:number; // intermediate character calculation + MDW?:number; // Excel's "Max Digit Width" unit, always integral + width:number; // width in Excel's "Max Digit Width", width*256 is integral + wpx?:number; // width in screen pixels + wch?:number; // intermediate character calculation + hidden:?boolean; // if true, the column is hidden }; ``` @@ -30,3 +31,26 @@ follow the priority order: 2) use `wpx` pixel width if available 3) use `wch` character count if available +#### Row Properties + +Excel internally stores row heights in points. The default resolution is 72 DPI +or 96 PPI, so the pixel and point size should agree. For different resolutions +they may not agree, so the library separates the concepts. + +The `!rows` array in each worksheet, if present, is a collection of `RowInfo` +objects which have the following properties: + +```typescript +type RowInfo = { + hpx?:number; // height in screen pixels + hpt?:number; // height in points + hidden:?boolean; // if true, the row is hidden +}; +``` + +Even though all of the information is made available, writers are expected to +follow the priority order: + +1) use `hpx` pixel height if available +2) use `hpt` point height if available + diff --git a/docbits/95_contrib.md b/docbits/95_contrib.md index 9a6e1ed..5878597 100644 --- a/docbits/95_contrib.md +++ b/docbits/95_contrib.md @@ -3,6 +3,25 @@ Due to the precarious nature of the Open Specifications Promise, it is very important to ensure code is cleanroom. Consult CONTRIBUTING.md +### Tests + +The `test_misc` target (`make test_misc` on Linux/OSX / `make misc` on Windows) +runs the targeted feature tests. It should take 5-10 seconds to perform feature +tests without testing against the entire test battery. New features should be +accompanied with tests for the relevant file formats and features. + +For tests involving the read side, an appropriate feature test would involve +reading an existing file and checking the resulting workbook object. If a +parameter is involved, files should be read with different values for the param +to verify that the feature is working as expected. + +For tests involving a new write feature which can already be parsed, appropriate +feature tests would involve writing a workbook with the feature and then opening +and verifying that the feature is preserved. + +For tests involving a new write feature without an existing read ability, please +add a feature test to the kitchen sink `tests/write.js`. + ### OSX/Linux The xlsx.js file is constructed from the files in the `bits` subdirectory. The diff --git a/misc/docs/SUMMARY.md b/misc/docs/SUMMARY.md index 613f855..fb32e99 100644 --- a/misc/docs/SUMMARY.md +++ b/misc/docs/SUMMARY.md @@ -29,6 +29,7 @@ * [Document Features](README.md#document-features) + [Formulae](README.md#formulae) + [Column Properties](README.md#column-properties) + + [Row Properties](README.md#row-properties) + [Hyperlinks](README.md#hyperlinks) + [Cell Comments](README.md#cell-comments) + [Sheet Visibility](README.md#sheet-visibility) @@ -69,6 +70,7 @@ * [Tested Environments](README.md#tested-environments) * [Test Files](README.md#test-files) - [Contributing](README.md#contributing) + * [Tests](README.md#tests) * [OSX/Linux](README.md#osxlinux) * [Windows](README.md#windows) - [License](README.md#license) diff --git a/test.js b/test.js index dcb9da7..b57dc0b 100644 --- a/test.js +++ b/test.js @@ -54,11 +54,12 @@ var paths = { cstxlsb: dir + 'comments_stress_test.xlsb', cstods: dir + 'comments_stress_test.ods', - cwxls: dir + 'column_width.xlsx', + cwxls: dir + 'column_width.xls', cwxls5: dir + 'column_width.biff5', cwxml: dir + 'column_width.xml', cwxlsx: dir + 'column_width.xlsx', - cwxlsb: dir + 'column_width.xlsx', + cwxlsb: dir + 'column_width.xlsb', + cwslk: dir + 'column_width.slk', dnsxls: dir + 'defined_names_simple.xls', dnsxml: dir + 'defined_names_simple.xml', @@ -101,6 +102,13 @@ var paths = { pmxlsx: dir + 'page_margins_2016.xlsx', pmxlsb: dir + 'page_margins_2016.xlsb', + rhxls: dir + 'row_height.xls', + rhxls5: dir + 'row_height.biff5', + rhxml: dir + 'row_height.xml', + rhxlsx: dir + 'row_height.xlsx', + rhxlsb: dir + 'row_height.xlsb', + rhslk: dir + 'row_height.slk', + svxls: dir + 'sheet_visibility.xls', svxls5: dir + 'sheet_visibility.xls', svxml: dir + 'sheet_visibility.xml', @@ -113,6 +121,10 @@ var paths = { swcxlsb: dir + '2013/apachepoi_SimpleWithComments.xlsx.xlsb' }; +var FSTPaths = [paths.fstxls, paths.fstxml, paths.fstxlsx, paths.fstxlsb, paths.fstods]; +var NFPaths = [paths.nfxls, paths.nfxml, paths.nfxlsx, paths.nfxlsb]; +var DTPaths = [paths.dtxls, paths.dtxml, paths.dtxlsx, paths.dtxlsb]; + var N1 = 'XLSX'; var N2 = 'XLSB'; var N3 = 'XLS'; @@ -144,7 +156,7 @@ function parsetest(x, wb, full, ext) { describe(x + ext + ' should generate JSON', function() { wb.SheetNames.forEach(function(ws, i) { it('#' + i + ' (' + ws + ')', function() { - X.utils.sheet_to_row_object_array(wb.Sheets[ws]); + X.utils.sheet_to_json(wb.Sheets[ws]); }); }); }); @@ -284,7 +296,6 @@ describe('parse options', function() { if(typeof before != 'undefined') before(bef); else it('before', bef); describe('cell', function() { - var FSTPaths = [paths.fstxls, paths.fstxml, paths.fstxlsx, paths.fstxlsb, paths.fstods]; it('XLSX should generate HTML by default', function() { var wb = X.readFile(paths.cstxlsx); var ws = wb.Sheets.Sheet1; @@ -348,7 +359,7 @@ describe('parse options', function() { }); }); it('should not generate number formats by default', function() { - [paths.nfxls, paths.nfxlsx, paths.nfxlsb].forEach(function(p) { + NFPaths.forEach(function(p) { var wb = X.readFile(p); wb.SheetNames.forEach(function(s) { var ws = wb.Sheets[s]; @@ -359,7 +370,7 @@ describe('parse options', function() { }); }); it('should generate number formats when requested', function() { - [paths.nfxls, paths.nfxlsx, paths.nfxlsb].forEach(function(p) { + NFPaths.forEach(function(p) { var wb = X.readFile(p, {cellNF: true}); wb.SheetNames.forEach(function(s) { var ws = wb.Sheets[s]; @@ -395,7 +406,7 @@ describe('parse options', function() { }); }); it('should not generate cell dates by default', function() { - [paths.dtxlsx, paths.dtxlsb, paths.dtxls, paths.dtxml].forEach(function(p) { + DTPaths.forEach(function(p) { var wb = X.readFile(p); wb.SheetNames.forEach(function(s) { var ws = wb.Sheets[s]; @@ -405,8 +416,8 @@ describe('parse options', function() { }); }); }); - it('XLSX should generate cell dates when requested', function() { - [paths.dtxlsx, paths.dtxlsb, paths.dtxls, paths.dtxml].forEach(function(p) { + it('should generate cell dates when requested', function() { + DTPaths.forEach(function(p) { var wb = X.readFile(paths.dtxlsx, {cellDates: true}); var found = false; wb.SheetNames.forEach(function(s) { @@ -818,7 +829,7 @@ describe('parse features', function() { }); describe('column properties', function() { - var wb1, wb2, wb3, wb4, wb5; + var wb1, wb2, wb3, wb4, wb5, wb6; var bef = (function() { X = require(modp); wb1 = X.readFile(paths.cwxlsx, {cellStyles:true}); @@ -826,21 +837,21 @@ describe('parse features', function() { wb3 = X.readFile(paths.cwxls, {cellStyles:true}); wb4 = X.readFile(paths.cwxls5, {cellStyles:true}); wb5 = X.readFile(paths.cwxml, {cellStyles:true}); + wb6 = X.readFile(paths.cwslk, {cellStyles:true}); }); if(typeof before != 'undefined') before(bef); else it('before', bef); it('should have "!cols"', function() { - assert(wb1.Sheets.Sheet1['!cols']); - assert(wb2.Sheets.Sheet1['!cols']); - assert(wb3.Sheets.Sheet1['!cols']); - assert(wb4.Sheets.Sheet1['!cols']); - assert(wb5.Sheets.Sheet1['!cols']); + [wb1, wb2, wb3, wb4, wb5, wb6].forEach(function(wb) { assert(wb.Sheets.Sheet1['!cols']); }); }); it('should have correct widths', function() { + /* SYLK rounds wch so skip non-integral */ [wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) { assert.equal(x[1].width, 0.1640625); assert.equal(x[2].width, 16.6640625); assert.equal(x[3].width, 1.6640625); + }); + [wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) { assert.equal(x[4].width, 4.83203125); assert.equal(x[5].width, 8.83203125); assert.equal(x[6].width, 12.83203125); @@ -848,10 +859,13 @@ describe('parse features', function() { }); }); it('should have correct pixels', function() { + /* SYLK rounds wch so skip non-integral */ [wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) { assert.equal(x[1].wpx, 1); assert.equal(x[2].wpx, 100); assert.equal(x[3].wpx, 10); + }); + [wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) { assert.equal(x[4].wpx, 29); assert.equal(x[5].wpx, 53); assert.equal(x[6].wpx, 77); @@ -860,6 +874,39 @@ describe('parse features', function() { }); }); + describe('row properties', function() { + var wb1, wb2, wb3, wb4, wb5, wb6; + var bef = (function() { + X = require(modp); + wb1 = X.readFile(paths.rhxlsx, {cellStyles:true}); + wb2 = X.readFile(paths.rhxlsb, {cellStyles:true}); + wb3 = X.readFile(paths.rhxls, {cellStyles:true}); + wb4 = X.readFile(paths.rhxls5, {cellStyles:true}); + wb5 = X.readFile(paths.rhxml, {cellStyles:true}); + wb6 = X.readFile(paths.rhslk, {cellStyles:true}); + }); + if(typeof before != 'undefined') before(bef); + else it('before', bef); + it('should have "!rows"', function() { + [wb1, wb2, wb3, wb4, wb5, wb6].forEach(function(wb) { assert(wb.Sheets.Sheet1['!rows']); }); + }); + it('should have correct points', function() { + [wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!rows']; }).forEach(function(x) { + assert.equal(x[1].hpt, 1); + assert.equal(x[2].hpt, 10); + assert.equal(x[3].hpt, 100); + }); + }); + it('should have correct pixels', function() { + [wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!rows']; }).forEach(function(x) { + /* note: at 96 PPI hpt == hpx */ + assert.equal(x[1].hpx, 1); + assert.equal(x[2].hpx, 10); + assert.equal(x[3].hpx, 100); + }); + }); + }); + describe('merge cells',function() { var wb1, wb2, wb3, wb4, wb5; var bef = (function() { @@ -910,7 +957,7 @@ describe('parse features', function() { var sheetName = 'Sheet1'; wb = X.readFile(paths.dtxlsx); ws = wb.Sheets[sheetName]; - var sheet = X.utils.sheet_to_row_object_array(ws); + var sheet = X.utils.sheet_to_json(ws); assert.equal(sheet[3]['てすと'], '2/14/14'); }); it('cellDates should not affect formatted text', function() { @@ -1210,9 +1257,8 @@ describe('roundtrip features', function() { }); }); }); - describe('should preserve features', function() { - it('merge cells', function() { - ["xlsx", "xlsb", "xlml", "ods"].forEach(function(f) { + describe('should preserve merge cells', function() { + ["xlsx", "xlsb", "xlml", "ods"].forEach(function(f) { it(f, function() { var wb1 = X.readFile(paths.mcxlsx); var wb2 = X.read(X.write(wb1,{bookType:f,type:'binary'}),{type:'binary'}); var m1 = wb1.Sheets.Merge['!merges'].map(X.utils.encode_range); @@ -1311,6 +1357,39 @@ describe('roundtrip features', function() { }); }); + describe('should preserve column properties', function() { [ + 'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk' + ].forEach(function(w) { it(w, function() { + var ws1 = X.utils.aoa_to_sheet([["hpx12", "hpt24", "hpx48", "hidden"]]); + ws1['!cols'] = [{wch:9},{wpx:100},{width:80},{hidden:true}]; + var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}}; + var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer"}), {type:"buffer", cellStyles:true}); + var ws2 = wb2.Sheets.Sheet1; + assert.equal(ws2['!cols'][3].hidden, true); + assert.equal(ws2['!cols'][0].wch, 9); + if(w == 'slk') return; + assert.equal(ws2['!cols'][1].wpx, 100); + /* xlml stores integral pixels -> approximate width */ + if(w == 'xlml') assert.equal(Math.round(ws2['!cols'][2].width), 80); + else assert.equal(ws2['!cols'][2].width, 80); + }); }); + }); + + describe('should preserve row properties', function() { [ + 'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk' + ].forEach(function(w) { it(w, function() { + var ws1 = X.utils.aoa_to_sheet([["hpx12"],["hpt24"],["hpx48"],["hidden"]]); + ws1['!rows'] = [{hpx:12},{hpt:24},{hpx:48},{hidden:true}]; + var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}}; + var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer"}), {type:"buffer", cellStyles:true}); + var ws2 = wb2.Sheets.Sheet1; + assert.equal(ws2['!rows'][0].hpx, 12); + assert.equal(ws2['!rows'][1].hpt, 24); + assert.equal(ws2['!rows'][2].hpx, 48); + assert.equal(ws2['!rows'][3].hidden, true); + }); }); + }); + describe('should preserve cell comments', function() { [ ['xlsx', paths.cstxlsx], ['xlsb', paths.cstxlsb], diff --git a/tests/core.js b/tests/core.js index 5bbdd57..0dee05d 100644 --- a/tests/core.js +++ b/tests/core.js @@ -80,6 +80,17 @@ var N2 = 'XLSB'; var N3 = 'XLS'; var N4 = 'XML'; +function get_cell(ws/*:Worksheet*/, addr/*:string*/) { + if(!Array.isArray(ws)) return ws[addr]; + var a = X.utils.decode_cell(addr); + return (ws[a.r]||[])[a.c]; +} + +function each_cell(ws, f) { + if(Array.isArray(ws)) ws.forEach(function(row) { if(row) row.forEach(f); }); + else Object.keys(ws).forEach(function(addr) { if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; f(ws[addr]); }); +} + /* comments_stress_test family */ function check_comments(wb) { var ws0 = wb.Sheets.Sheet2; @@ -516,12 +527,12 @@ describe('parse features', function() { var wb4=X.read(fs.readFileSync(paths.swcxml), {type:"binary"}); [wb1,wb2,wb3,wb4].map(function(wb) { return wb.Sheets[sheet]; }).forEach(function(ws, i) { - assert.equal(ws.B1.c.length, 1,"must have 1 comment"); - assert.equal(ws.B1.c[0].a, "Yegor Kozlov","must have the same author"); - assert.equal(ws.B1.c[0].t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"), "Yegor Kozlov:\nfirst cell", "must have the concatenated texts"); + assert.equal(get_cell(ws, "B1").c.length, 1,"must have 1 comment"); + assert.equal(get_cell(ws, "B1").c[0].a, "Yegor Kozlov","must have the same author"); + assert.equal(get_cell(ws, "B1").c[0].t, "Yegor Kozlov:\nfirst cell", "must have the concatenated texts"); if(i > 0) return; - assert.equal(ws.B1.c[0].r, 'Yegor Kozlov:\r\nfirst cell', "must have the rich text representation"); - assert.equal(ws.B1.c[0].h, 'Yegor Kozlov:
first cell
', "must have the html representation"); + assert.equal(get_cell(ws, "B1").c[0].r, 'Yegor Kozlov:\r\nfirst cell', "must have the rich text representation"); + assert.equal(get_cell(ws, "B1").c[0].h, 'Yegor Kozlov:
first cell
', "must have the html representation"); }); }); [ @@ -612,11 +623,7 @@ describe('parse features', function() { if(typeof before != 'undefined') before(bef); else it('before', bef); it('should have "!cols"', function() { - assert(wb1.Sheets.Sheet1['!cols']); - assert(wb2.Sheets.Sheet1['!cols']); - assert(wb3.Sheets.Sheet1['!cols']); - assert(wb4.Sheets.Sheet1['!cols']); - assert(wb5.Sheets.Sheet1['!cols']); + [wb1, wb2, wb3, wb4, wb5].forEach(function(wb) { assert(wb.Sheets.Sheet1['!cols']); }); }); it('should have correct widths', function() { [wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) { @@ -809,6 +816,56 @@ describe('parse features', function() { }); }); +describe('write features', function() { + describe('props', function() { + describe('core', function() { + var ws, baseprops; + var bef = (function() { + X = require(modp); + ws = X.utils.aoa_to_sheet([["a","b","c"],[1,2,3]]); + baseprops = { + Category: "C4tegory", + ContentStatus: "C0ntentStatus", + Keywords: "K3ywords", + LastAuthor: "L4stAuthor", + LastPrinted: "L4stPrinted", + RevNumber: 6969, + AppVersion: 69, + Author: "4uth0r", + Comments: "C0mments", + Identifier: "1d", + Language: "L4nguage", + Subject: "Subj3ct", + Title: "T1tle" + }; + }); + if(typeof before != 'undefined') before(bef); + else it('before', bef); + ['xlml', 'xlsx', 'xlsb'].forEach(function(w) { it(w, function() { + wb = { + Props: {}, + SheetNames: ["Sheet1"], + Sheets: {Sheet1: ws} + }; + Object.keys(baseprops).forEach(function(k) { wb.Props[k] = baseprops[k]; }); + var wb2 = X.read(X.write(wb, {bookType:w, type:"binary"}), {type:"binary"}); + Object.keys(baseprops).forEach(function(k) { assert.equal(baseprops[k], wb2.Props[k]); }); + var wb3 = X.read(X.write(wb2, {bookType:w, type:"binary", Props: {Author:"SheetJS"}}), {type:"binary"}); + assert.equal("SheetJS", wb3.Props.Author); + }); }); + }); + }); + describe('HTML', function() { + it('should use `h` value when present', function() { + var sheet = X.utils.aoa_to_sheet([["abc"]]); + get_cell(sheet, "A1").h = "abc"; + var wb = {SheetNames:["Sheet1"], Sheets:{Sheet1:sheet}}; + var str = X.write(wb, {bookType:"html", type:"binary"}); + assert(str.indexOf("abc") > 0); + }); + }); +}); + function seq(end, start) { var s = start || 0; var o = new Array(end - s); @@ -895,8 +952,8 @@ describe('roundtrip features', function() { describe('should preserve hyperlink', function() { [ ['xlml', paths.hlxml], - //['xlsx', paths.hlxlsx], // TODO - //['xlsb', paths.hlxlsb] // TODO + ['xlsx', paths.hlxlsx], + ['xlsb', paths.hlxlsb] ].forEach(function(w) { it(w[0], function() { var wb1 = X.read(fs.readFileSync(w[1]), {type:"binary"}); @@ -926,6 +983,39 @@ describe('roundtrip features', function() { }); }); + describe('should preserve column properties', function() { [ + 'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk' + ].forEach(function(w) { it(w, function() { + var ws1 = X.utils.aoa_to_sheet([["hpx12", "hpt24", "hpx48", "hidden"]]); + ws1['!cols'] = [{wch:9},{wpx:100},{width:80},{hidden:true}]; + var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}}; + var wb2 = X.read(X.write(wb1, {bookType:w, type:"binary"}), {type:"binary", cellStyles:true}); + var ws2 = wb2.Sheets.Sheet1; + assert.equal(ws2['!cols'][3].hidden, true); + assert.equal(ws2['!cols'][0].wch, 9); + if(w == 'slk') return; + assert.equal(ws2['!cols'][1].wpx, 100); + /* xlml stores integral pixels -> approximate width */ + if(w == 'xlml') assert.equal(Math.round(ws2['!cols'][2].width), 80); + else assert.equal(ws2['!cols'][2].width, 80); + }); }); + }); + + describe('should preserve row properties', function() { [ + 'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk' + ].forEach(function(w) { it(w, function() { + var ws1 = X.utils.aoa_to_sheet([["hpx12"],["hpt24"],["hpx48"],["hidden"]]); + ws1['!rows'] = [{hpx:12},{hpt:24},{hpx:48},{hidden:true}]; + var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}}; + var wb2 = X.read(X.write(wb1, {bookType:w, type:"binary"}), {type:"binary", cellStyles:true}); + var ws2 = wb2.Sheets.Sheet1; + assert.equal(ws2['!rows'][0].hpx, 12); + assert.equal(ws2['!rows'][1].hpt, 24); + assert.equal(ws2['!rows'][2].hpx, 48); + assert.equal(ws2['!rows'][3].hidden, true); + }); }); + }); + describe('should preserve cell comments', function() { [ ['xlsx', paths.cstxlsx], ['xlsb', paths.cstxlsb], diff --git a/tests/write.js b/tests/write.js index 105df79..af42609 100644 --- a/tests/write.js +++ b/tests/write.js @@ -16,14 +16,15 @@ var ws_name = "SheetJS"; var wscols = [ {wch:6}, // "characters" {wpx:50}, // "pixels" - {wch:10}, - {wpx:125} + /*{wch:10}*/, + {hidden:true} ]; +/* At 96 PPI, 1 pt = 1 px */ var wsrows = []; wsrows[0] = {hpt: 12}; // "points" wsrows[1] = {hpx: 16}; // "pixels" -wsrows[2] = {hpt: 18}; +//wsrows[2] = {hpt: 18}; wsrows[3] = {hpx: 24}; wsrows[4] = {hidden:true}; // hide row wsrows[5] = {hidden:false}; @@ -108,6 +109,10 @@ ws['A4'].c.push({a:"SheetJS",t:"I'm a little comment, short and stout!\n\nWell, /* TEST: sheet protection */ ws['!protect'] = { password:"password", + /* enable formatting rows and columns */ + formatRows:0, + formatColumns:0, + /* disable editing objects and scenarios */ objects:1, scenarios:1 }; diff --git a/xlsx.flow.js b/xlsx.flow.js index d95a144..c9009d5 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -4069,14 +4069,19 @@ function parse_ExtSST(blob, length) { } -/* 2.4.221 TODO*/ +/* 2.4.221 TODO: check BIFF2-4 */ function parse_Row(blob, length) { - var rw = blob.read_shift(2), col = blob.read_shift(2), Col = blob.read_shift(2), rht = blob.read_shift(2); - blob.read_shift(4); // reserved(2), unused(2) + var z = ({}/*:any*/); + z.r = blob.read_shift(2); + z.c = blob.read_shift(2); + z.cnt = blob.read_shift(2) - z.c; + var miyRw = blob.read_shift(2); + blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags - blob.read_shift(1); // reserved - blob.read_shift(2); //ixfe, other flags - return {r:rw, c:col, cnt:Col-col}; + blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x20) z.hidden = true; + if(flags & 0x40) z.hpt = miyRw / 20; + return z; } @@ -5113,19 +5118,19 @@ var SYLK = (function() { var records = str.split(/[\n\r]+/), R = -1, C = -1, ri = 0, rj = 0, arr = []; var formats = []; var next_cell_format = null; + var sht = {}, rowinfo = [], colinfo = [], cw = []; + var Mval = 0, j; for (; ri !== records.length; ++ri) { + Mval = 0; var record = records[ri].trim().split(";"); var RT = record[0], val; - if(RT === 'P') for(rj=1; rj 0) { rowinfo[R].hpt = Mval; rowinfo[R].hpx = pt2px(Mval); } + else if(Mval == 0) rowinfo[R].hidden = true; + } break; + default: break; } } + if(rowinfo.length > 0) sht['!rows'] = rowinfo; + if(colinfo.length > 0) sht['!cols'] = colinfo; + arr[arr.length] = sht; return arr; } - function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(sylk_to_aoa(str, opts), opts); } + function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { + var aoa = sylk_to_aoa(str, opts); + var ws = aoa.pop(); + var o = aoa_to_sheet(aoa, opts); + keys(ws).forEach(function(k) { o[k] = ws[k]; }); + return o; + } function sylk_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(sylk_to_sheet(str, opts), opts); } @@ -5164,11 +5194,40 @@ var SYLK = (function() { return o; } + function write_ws_cols_sylk(out, cols) { + cols.forEach(function(col, i) { + var rec = "F;W" + (i+1) + " " + (i+1) + " "; + if(col.hidden) rec += "0"; + else { + if(typeof col.width == 'number') col.wpx = width2px(col.width); + if(typeof col.wpx == 'number') col.wch = px2char(col.wpx); + if(typeof col.wch == 'number') rec += Math.round(col.wch); + } + if(rec.charAt(rec.length - 1) != " ") out.push(rec); + }); + } + + function write_ws_rows_sylk(out, rows) { + rows.forEach(function(row, i) { + var rec = "F;"; + if(row.hidden) rec += "M0;"; + else if(row.hpt) rec += "M" + 20 * row.hpt + ";"; + else if(row.hpx) rec += "M" + 20 * px2pt(row.hpx) + ";"; + if(rec.length > 2) out.push(rec + "R" + (i+1)); + }); + } + function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/)/*:string*/ { var preamble/*:Array*/ = ["ID;PWXL;N;E"], o/*:Array*/ = []; - preamble.push("P;PGeneral"); var r = decode_range(ws['!ref']), cell/*:Cell*/; var dense = Array.isArray(ws); + var RS = "\r\n"; + + preamble.push("P;PGeneral"); + preamble.push("F;P0;DG0G8;M255"); + if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']); + if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']); + for(var R = r.s.r; R <= r.e.r; ++R) { for(var C = r.s.c; C <= r.e.c; ++C) { var coord = encode_cell({r:R,c:C}); @@ -5177,8 +5236,6 @@ var SYLK = (function() { o.push(write_ws_cell_sylk(cell, ws, R, C, opts)); } } - preamble.push("F;P0;DG0G8;M255"); - var RS = "\r\n"; return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS; } @@ -6388,13 +6445,17 @@ function process_col(coll/*:ColInfo*/) { coll.wch = px2char(coll.wpx); coll.width = char2width(coll.wch); coll.MDW = MDW; + } else if(typeof coll.wch == 'number') { + coll.width = char2width(coll.wch); + coll.wpx = width2px(coll.width); + coll.MDW = MDW; } if(coll.customWidth) delete coll.customWidth; } -var DEF_DPI = 96, DPI = DEF_DPI; -function px2pt(px) { return px * 72 / DPI; } -function pt2px(pt) { return pt * DPI / 72; } +var DEF_PPI = 96, PPI = DEF_PPI; +function px2pt(px) { return px * 96 / PPI; } +function pt2px(pt) { return pt * PPI / 96; } /* [MS-EXSPXML3] 2.4.54 ST_enmPattern */ var XLMLPatternTypeMap = { @@ -9922,13 +9983,14 @@ function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ { function col_obj_w(C/*:number*/, col) { var p = ({min:C+1,max:C+1}/*:any*/); /* wch (chars), wpx (pixels) */ - var width = -1; + var wch = -1; if(col.MDW) MDW = col.MDW; if(col.width != null) p.customWidth = 1; - else if(col.wpx != null) width = px2char(col.wpx); - else if(col.wch != null) width = col.wch; - if(width > -1) { p.width = char2width(width); p.customWidth = 1; } - else p.width = col.width; + else if(col.wpx != null) wch = px2char(col.wpx); + else if(col.wch != null) wch = col.wch; + if(wch > -1) { p.width = char2width(wch); p.customWidth = 1; } + else if(col.width != null) p.width = col.width; + if(col.hidden) p.hidden = true; return p; } @@ -10148,6 +10210,7 @@ function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { var coll = parsexmltag(cols[coli], true); + if(coll.hidden) coll.hidden = parsexmlbool(coll.hidden); var colm=parseInt(coll.min, 10)-1, colM=parseInt(coll.max,10)-1; delete coll.min; delete coll.max; coll.width = +coll.width; if(!seencol && coll.width) { seencol = true; find_mdw_colw(coll.width); } @@ -10174,6 +10237,12 @@ function write_ws_xml_autofilter(data)/*:string*/ { return writextag("autoFilter", null, {ref:data.ref}); } +/* 18.3.1.88 sheetViews CT_SheetViews */ +/* 18.3.1.87 sheetView CT_SheetView */ +function write_ws_xml_sheetviews(ws, opts, idx, wb)/*:string*/ { + return writextag("sheetViews", writextag("sheetView", null, {workbookViewId:"0"}), {}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -10225,13 +10294,14 @@ var parse_ws_xml_data = (function parse_ws_xml_data_factory() { var match_v = matchtag("v"), match_f = matchtag("f"); return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { - var ri = 0, x = "", cells = [], cref = [], idx = 0, i=0, cc=0, d="", p/*:any*/; + var ri = 0, x = "", cells = [], cref = [], idx=0, i=0, cc=0, d="", p/*:any*/; var tag, tagr = 0, tagc = 0; var sstr, ftag; var fmtid = 0, fillid = 0, do_format = Array.isArray(styles.CellXf), cf; var arrayf = []; var sharedf = []; var dense = Array.isArray(s); + var rows = [], rowobj = {}, rowrite = false; for(var marr = sdata.split(rowregex), mt = 0, marrlen = marr.length; mt != marrlen; ++mt) { x = marr[mt].trim(); var xlen = x.length; @@ -10245,6 +10315,13 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { if(guess.s.r > tagr - 1) guess.s.r = tagr - 1; if(guess.e.r < tagr - 1) guess.e.r = tagr - 1; + if(opts && opts.cellStyles) { + rowobj = {}; rowrite = false; + if(tag.ht) { rowrite = true; rowobj.hpt = parseFloat(tag.ht); rowobj.hpx = pt2px(rowobj.hpt); } + if(tag.hidden == "1") { rowrite = true; rowobj.hidden = true; } + if(rowrite) rows[tagr-1] = rowobj; + } + /* 18.3.1.4 c CT_Cell */ cells = x.substr(ri).split(cellregex); for(ri = 0; ri != cells.length; ++ri) { @@ -10353,6 +10430,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } else s[tag.r] = p; } } + if(rows.length > 0) s['!rows'] = rows; }; })(); function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/, rels)/*:string*/ { @@ -10403,7 +10481,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { o[o.length] = (writextag('dimension', null, {'ref': ref})); - /* sheetViews */ + o[o.length] = write_ws_xml_sheetviews(ws, opts, idx, wb); /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); @@ -10453,7 +10531,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { delete ws['!links']; /* printOptions */ - if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']) + if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']); /* pageSetup */ var hfidx = o.length; @@ -10493,20 +10571,38 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { /* [MS-XLSB] 2.4.718 BrtRowHdr */ function parse_BrtRowHdr(data, length) { - var z = ([]/*:any*/); + var z = ({}/*:any*/); + var tgt = data.l + length; z.r = data.read_shift(4); - data.l += length-4; + data.l += 4; // TODO: ixfe + var miyRw = data.read_shift(2); + data.l += 1; // TODO: top/bot padding + var flags = data.read_shift(1); + data.l = tgt; + if(flags & 0x10) z.hidden = true; + if(flags & 0x20) z.hpt = miyRw / 20; return z; } function write_BrtRowHdr(R/*:number*/, range, ws) { var o = new_buf(17+8*16); + var row = (ws['!rows']||[])[R]||{}; o.write_shift(4, R); - /* TODO: flags styles */ - o.write_shift(4, 0); - o.write_shift(2, 0x0140); - o.write_shift(2, 0); - o.write_shift(1, 0); + o.write_shift(4, 0); /* TODO: ixfe */ + + var miyRw = 0x0140; + if(row.hpx) miyRw = px2pt(row.hpx) * 20; + else if(row.hpt) miyRw = row.hpt * 20; + o.write_shift(2, miyRw); + + o.write_shift(1, 0); /* top/bot padding */ + + var flags = 0x0; + if(row.hidden) flags |= 0x10; + if(row.hpx || row.hpt) flags |= 0x20; + o.write_shift(1, flags); + + o.write_shift(1, 0); /* phonetic guide */ /* [MS-XLSB] 2.5.8 BrtColSpan explains the mechanism */ var ncolspan = 0, lcs = o.l; @@ -10774,9 +10870,12 @@ function write_BrtColInfo(C/*:number*/, col, o) { var p = col_obj_w(C, col); o.write_shift(-4, C); o.write_shift(-4, C); - o.write_shift(4, p.width * 256); + o.write_shift(4, (p.width || 10) * 256); o.write_shift(4, 0/*ixfe*/); // style - o.write_shift(1, 2); // bit flag + var flags = 0; + if(col.hidden) flags |= 0x01; + if(typeof p.width == 'number') flags |= 0x02; + o.write_shift(1, flags); // bit flag o.write_shift(1, 0); // bit flag return o; } @@ -10804,6 +10903,24 @@ function write_BrtMargins(margins, o) { return o; } +/* [MS-XLSB] 2.4.292 BrtBeginWsView */ +function write_BrtBeginWsView(ws, o) { + if(o == null) o = new_buf(30); + o.write_shift(2, 924); // bit flag + o.write_shift(4, 0); + o.write_shift(4, 0); // view first row + o.write_shift(4, 0); // view first col + o.write_shift(1, 0); // gridline color ICV + o.write_shift(1, 0); + o.write_shift(2, 0); + o.write_shift(2, 100); // zoom scale + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(4, 0); // workbook view id + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -10826,9 +10943,8 @@ function write_BrtSheetProtection(sp, o) { ["pivotTables", true], // fPivotTables ["selectUnlockedCells", false] // fSelUnlockedCells ].forEach(function(n) { - o.write_shift(4, 1); - if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); - else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + if(n[1]) o.write_shift(4, sp[n[0]] != null && !sp[n[0]] ? 1 : 0); + else o.write_shift(4, sp[n[0]] != null && sp[n[0]] ? 0 : 1); }); return o; } @@ -10875,6 +10991,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(opts.sheetRows && opts.sheetRows <= row.r) end=true; rr = encode_row(R = row.r); opts['!row'] = row.r; + if(val.hidden || val.hpt) { + if(val.hpt) val.hpx = pt2px(val.hpt); + rowinfo[val.r] = val; + } break; case 0x0002: /* 'BrtCellRk' */ @@ -10972,7 +11092,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { case 0x003C: /* 'BrtColInfo' */ if(!opts.cellStyles) break; while(val.e >= val.s) { - colinfo[val.e--] = { width: val.w/256 }; + colinfo[val.e--] = { width: val.w/256, hidden: !!(val.flags & 0x01) }; if(!seencol) { seencol = true; find_mdw_colw(val.w/256); } process_col(colinfo[val.e+1]); } @@ -11188,6 +11308,21 @@ function write_AUTOFILTER(ba, ws) { write_record(ba, "BrtEndAFilter"); } +function write_WSVIEWS2(ba, ws) { + write_record(ba, "BrtBeginWsViews"); + { /* 1*WSVIEW2 */ + /* [ACUID] */ + write_record(ba, "BrtBeginWsView", write_BrtBeginWsView(ws)); + /* [BrtPane] */ + /* *4BrtSel */ + /* *4SXSELECT */ + /* *FRT */ + write_record(ba, "BrtEndWsView"); + } + /* *FRT */ + write_record(ba, "BrtEndWsViews"); +} + function write_SHEETPROTECT(ba, ws) { if(!ws['!protect']) return; /* [BrtSheetProtectionIso] */ @@ -11204,7 +11339,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); - /* [WSVIEWS2] */ + write_WSVIEWS2(ba, ws); /* [WSFMTINFO] */ write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); @@ -12224,7 +12359,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { var comments = [], comment = {}; var cstys = [], csty, seencol = false; var arrayf = []; - var rowinfo = []; + var rowinfo = [], rowobj = {}; var Workbook = { Sheets:[] }, wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); @@ -12289,6 +12424,12 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } else { row = xlml_parsexmltag(Rn[0]); if(row.Index) r = +row.Index - 1; + rowobj = {}; + if(row.AutoFitHeight == "0") { + rowobj.hpx = parseInt(row.Height, 10); rowobj.hpt = px2pt(rowobj.hpx); + rowinfo[r] = rowobj; + } + if(row.Hidden == "1") { rowobj.hidden = true; rowinfo[r] = rowobj; } } break; case 'Worksheet': /* TODO: read range from FullRows/FullColumns */ @@ -12339,9 +12480,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Column': if(state[state.length-1][0] !== 'Table') break; csty = xlml_parsexmltag(Rn[0]); - csty.wpx = parseInt(csty.Width, 10); + if(csty.Hidden) { csty.hidden = true; delete csty.Hidden; } + if(csty.Width) csty.wpx = parseInt(csty.Width, 10); if(!seencol && csty.wpx > 10) { - seencol = true; find_mdw_wpx(csty.wpx); + seencol = true; MDW = DEF_MDW; //find_mdw_wpx(csty.wpx); for(var _col = 0; _col < cstys.length; ++_col) if(cstys[_col]) process_col(cstys[_col]); } if(seencol) process_col(csty); @@ -12478,7 +12620,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Color': break; case 'Index': break; case 'RGB': break; - case 'PixelsPerInch': break; + case 'PixelsPerInch': break; // TODO: set PPI case 'TargetScreenSize': break; case 'ReadOnlyRecommended': break; default: seen = false; @@ -13030,6 +13172,15 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ return writextag("Cell", m, attr); } +function write_ws_xlml_row(R/*:number*/, row)/*:string*/ { + var o = ''; +} /* TODO */ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/)/*:string*/ { if(!ws['!ref']) return ""; @@ -13037,12 +13188,17 @@ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbo var marr = ws['!merges'] || [], mi = 0; var o = []; if(ws['!cols']) ws['!cols'].forEach(function(n, i) { + process_col(n); + var w = !!n.width; var p = col_obj_w(i, n); - o.push(writextag("Column",null, {"ss:Index":i+1, "ss:Width":width2px(p.width)})); + var k = {"ss:Index":i+1}; + if(w) k['ss:Width'] = width2px(p.width); + if(n.hidden) k['ss:Hidden']="1"; + o.push(writextag("Column",null,k)); }); var dense = Array.isArray(ws); for(var R = range.s.r; R <= range.e.r; ++R) { - var row = ['']; + var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])]; for(var C = range.s.c; C <= range.e.c; ++C) { var skip = false; for(mi = 0; mi != marr.length; ++mi) { @@ -13597,7 +13753,14 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { process_col(colinfo[val.e+1]); } } break; - case 'Row': break; // TODO + case 'Row': { + var rowobj = {}; + if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } + if(val.hpt) { + rowinfo[val.r] = rowobj; + rowobj.hpt = val.hpt; rowobj.hpx = pt2px(val.hpt); + } + } break; case 'LeftMargin': case 'RightMargin': diff --git a/xlsx.js b/xlsx.js index de5700f..59561e6 100644 --- a/xlsx.js +++ b/xlsx.js @@ -4008,14 +4008,19 @@ function parse_ExtSST(blob, length) { } -/* 2.4.221 TODO*/ +/* 2.4.221 TODO: check BIFF2-4 */ function parse_Row(blob, length) { - var rw = blob.read_shift(2), col = blob.read_shift(2), Col = blob.read_shift(2), rht = blob.read_shift(2); - blob.read_shift(4); // reserved(2), unused(2) + var z = ({}); + z.r = blob.read_shift(2); + z.c = blob.read_shift(2); + z.cnt = blob.read_shift(2) - z.c; + var miyRw = blob.read_shift(2); + blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags - blob.read_shift(1); // reserved - blob.read_shift(2); //ixfe, other flags - return {r:rw, c:col, cnt:Col-col}; + blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x20) z.hidden = true; + if(flags & 0x40) z.hpt = miyRw / 20; + return z; } @@ -5052,19 +5057,19 @@ var SYLK = (function() { var records = str.split(/[\n\r]+/), R = -1, C = -1, ri = 0, rj = 0, arr = []; var formats = []; var next_cell_format = null; + var sht = {}, rowinfo = [], colinfo = [], cw = []; + var Mval = 0, j; for (; ri !== records.length; ++ri) { + Mval = 0; var record = records[ri].trim().split(";"); var RT = record[0], val; - if(RT === 'P') for(rj=1; rj 0) { rowinfo[R].hpt = Mval; rowinfo[R].hpx = pt2px(Mval); } + else if(Mval == 0) rowinfo[R].hidden = true; + } break; + default: break; } } + if(rowinfo.length > 0) sht['!rows'] = rowinfo; + if(colinfo.length > 0) sht['!cols'] = colinfo; + arr[arr.length] = sht; return arr; } - function sylk_to_sheet(str, opts) { return aoa_to_sheet(sylk_to_aoa(str, opts), opts); } + function sylk_to_sheet(str, opts) { + var aoa = sylk_to_aoa(str, opts); + var ws = aoa.pop(); + var o = aoa_to_sheet(aoa, opts); + keys(ws).forEach(function(k) { o[k] = ws[k]; }); + return o; + } function sylk_to_workbook(str, opts) { return sheet_to_workbook(sylk_to_sheet(str, opts), opts); } @@ -5103,11 +5133,40 @@ var SYLK = (function() { return o; } + function write_ws_cols_sylk(out, cols) { + cols.forEach(function(col, i) { + var rec = "F;W" + (i+1) + " " + (i+1) + " "; + if(col.hidden) rec += "0"; + else { + if(typeof col.width == 'number') col.wpx = width2px(col.width); + if(typeof col.wpx == 'number') col.wch = px2char(col.wpx); + if(typeof col.wch == 'number') rec += Math.round(col.wch); + } + if(rec.charAt(rec.length - 1) != " ") out.push(rec); + }); + } + + function write_ws_rows_sylk(out, rows) { + rows.forEach(function(row, i) { + var rec = "F;"; + if(row.hidden) rec += "M0;"; + else if(row.hpt) rec += "M" + 20 * row.hpt + ";"; + else if(row.hpx) rec += "M" + 20 * px2pt(row.hpx) + ";"; + if(rec.length > 2) out.push(rec + "R" + (i+1)); + }); + } + function sheet_to_sylk(ws, opts) { var preamble = ["ID;PWXL;N;E"], o = []; - preamble.push("P;PGeneral"); var r = decode_range(ws['!ref']), cell; var dense = Array.isArray(ws); + var RS = "\r\n"; + + preamble.push("P;PGeneral"); + preamble.push("F;P0;DG0G8;M255"); + if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']); + if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']); + for(var R = r.s.r; R <= r.e.r; ++R) { for(var C = r.s.c; C <= r.e.c; ++C) { var coord = encode_cell({r:R,c:C}); @@ -5116,8 +5175,6 @@ var SYLK = (function() { o.push(write_ws_cell_sylk(cell, ws, R, C, opts)); } } - preamble.push("F;P0;DG0G8;M255"); - var RS = "\r\n"; return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS; } @@ -6327,13 +6384,17 @@ function process_col(coll) { coll.wch = px2char(coll.wpx); coll.width = char2width(coll.wch); coll.MDW = MDW; + } else if(typeof coll.wch == 'number') { + coll.width = char2width(coll.wch); + coll.wpx = width2px(coll.width); + coll.MDW = MDW; } if(coll.customWidth) delete coll.customWidth; } -var DEF_DPI = 96, DPI = DEF_DPI; -function px2pt(px) { return px * 72 / DPI; } -function pt2px(pt) { return pt * DPI / 72; } +var DEF_PPI = 96, PPI = DEF_PPI; +function px2pt(px) { return px * 96 / PPI; } +function pt2px(pt) { return pt * PPI / 96; } /* [MS-EXSPXML3] 2.4.54 ST_enmPattern */ var XLMLPatternTypeMap = { @@ -9860,13 +9921,14 @@ function get_sst_id(sst, str) { function col_obj_w(C, col) { var p = ({min:C+1,max:C+1}); /* wch (chars), wpx (pixels) */ - var width = -1; + var wch = -1; if(col.MDW) MDW = col.MDW; if(col.width != null) p.customWidth = 1; - else if(col.wpx != null) width = px2char(col.wpx); - else if(col.wch != null) width = col.wch; - if(width > -1) { p.width = char2width(width); p.customWidth = 1; } - else p.width = col.width; + else if(col.wpx != null) wch = px2char(col.wpx); + else if(col.wch != null) wch = col.wch; + if(wch > -1) { p.width = char2width(wch); p.customWidth = 1; } + else if(col.width != null) p.width = col.width; + if(col.hidden) p.hidden = true; return p; } @@ -10086,6 +10148,7 @@ function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { var coll = parsexmltag(cols[coli], true); + if(coll.hidden) coll.hidden = parsexmlbool(coll.hidden); var colm=parseInt(coll.min, 10)-1, colM=parseInt(coll.max,10)-1; delete coll.min; delete coll.max; coll.width = +coll.width; if(!seencol && coll.width) { seencol = true; find_mdw_colw(coll.width); } @@ -10112,6 +10175,12 @@ function write_ws_xml_autofilter(data) { return writextag("autoFilter", null, {ref:data.ref}); } +/* 18.3.1.88 sheetViews CT_SheetViews */ +/* 18.3.1.87 sheetView CT_SheetView */ +function write_ws_xml_sheetviews(ws, opts, idx, wb) { + return writextag("sheetViews", writextag("sheetView", null, {workbookViewId:"0"}), {}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -10163,13 +10232,14 @@ var parse_ws_xml_data = (function parse_ws_xml_data_factory() { var match_v = matchtag("v"), match_f = matchtag("f"); return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { - var ri = 0, x = "", cells = [], cref = [], idx = 0, i=0, cc=0, d="", p; + var ri = 0, x = "", cells = [], cref = [], idx=0, i=0, cc=0, d="", p; var tag, tagr = 0, tagc = 0; var sstr, ftag; var fmtid = 0, fillid = 0, do_format = Array.isArray(styles.CellXf), cf; var arrayf = []; var sharedf = []; var dense = Array.isArray(s); + var rows = [], rowobj = {}, rowrite = false; for(var marr = sdata.split(rowregex), mt = 0, marrlen = marr.length; mt != marrlen; ++mt) { x = marr[mt].trim(); var xlen = x.length; @@ -10183,6 +10253,13 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { if(guess.s.r > tagr - 1) guess.s.r = tagr - 1; if(guess.e.r < tagr - 1) guess.e.r = tagr - 1; + if(opts && opts.cellStyles) { + rowobj = {}; rowrite = false; + if(tag.ht) { rowrite = true; rowobj.hpt = parseFloat(tag.ht); rowobj.hpx = pt2px(rowobj.hpt); } + if(tag.hidden == "1") { rowrite = true; rowobj.hidden = true; } + if(rowrite) rows[tagr-1] = rowobj; + } + /* 18.3.1.4 c CT_Cell */ cells = x.substr(ri).split(cellregex); for(ri = 0; ri != cells.length; ++ri) { @@ -10291,6 +10368,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } else s[tag.r] = p; } } + if(rows.length > 0) s['!rows'] = rows; }; })(); function write_ws_xml_data(ws, opts, idx, wb, rels) { @@ -10341,7 +10419,7 @@ function write_ws_xml(idx, opts, wb, rels) { o[o.length] = (writextag('dimension', null, {'ref': ref})); - /* sheetViews */ + o[o.length] = write_ws_xml_sheetviews(ws, opts, idx, wb); /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); @@ -10391,7 +10469,7 @@ function write_ws_xml(idx, opts, wb, rels) { delete ws['!links']; /* printOptions */ - if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']) + if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']); /* pageSetup */ var hfidx = o.length; @@ -10431,20 +10509,38 @@ function write_ws_xml(idx, opts, wb, rels) { /* [MS-XLSB] 2.4.718 BrtRowHdr */ function parse_BrtRowHdr(data, length) { - var z = ([]); + var z = ({}); + var tgt = data.l + length; z.r = data.read_shift(4); - data.l += length-4; + data.l += 4; // TODO: ixfe + var miyRw = data.read_shift(2); + data.l += 1; // TODO: top/bot padding + var flags = data.read_shift(1); + data.l = tgt; + if(flags & 0x10) z.hidden = true; + if(flags & 0x20) z.hpt = miyRw / 20; return z; } function write_BrtRowHdr(R, range, ws) { var o = new_buf(17+8*16); + var row = (ws['!rows']||[])[R]||{}; o.write_shift(4, R); - /* TODO: flags styles */ - o.write_shift(4, 0); - o.write_shift(2, 0x0140); - o.write_shift(2, 0); - o.write_shift(1, 0); + o.write_shift(4, 0); /* TODO: ixfe */ + + var miyRw = 0x0140; + if(row.hpx) miyRw = px2pt(row.hpx) * 20; + else if(row.hpt) miyRw = row.hpt * 20; + o.write_shift(2, miyRw); + + o.write_shift(1, 0); /* top/bot padding */ + + var flags = 0x0; + if(row.hidden) flags |= 0x10; + if(row.hpx || row.hpt) flags |= 0x20; + o.write_shift(1, flags); + + o.write_shift(1, 0); /* phonetic guide */ /* [MS-XLSB] 2.5.8 BrtColSpan explains the mechanism */ var ncolspan = 0, lcs = o.l; @@ -10712,9 +10808,12 @@ function write_BrtColInfo(C, col, o) { var p = col_obj_w(C, col); o.write_shift(-4, C); o.write_shift(-4, C); - o.write_shift(4, p.width * 256); + o.write_shift(4, (p.width || 10) * 256); o.write_shift(4, 0/*ixfe*/); // style - o.write_shift(1, 2); // bit flag + var flags = 0; + if(col.hidden) flags |= 0x01; + if(typeof p.width == 'number') flags |= 0x02; + o.write_shift(1, flags); // bit flag o.write_shift(1, 0); // bit flag return o; } @@ -10742,6 +10841,24 @@ function write_BrtMargins(margins, o) { return o; } +/* [MS-XLSB] 2.4.292 BrtBeginWsView */ +function write_BrtBeginWsView(ws, o) { + if(o == null) o = new_buf(30); + o.write_shift(2, 924); // bit flag + o.write_shift(4, 0); + o.write_shift(4, 0); // view first row + o.write_shift(4, 0); // view first col + o.write_shift(1, 0); // gridline color ICV + o.write_shift(1, 0); + o.write_shift(2, 0); + o.write_shift(2, 100); // zoom scale + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(2, 0); + o.write_shift(4, 0); // workbook view id + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -10764,9 +10881,8 @@ function write_BrtSheetProtection(sp, o) { ["pivotTables", true], // fPivotTables ["selectUnlockedCells", false] // fSelUnlockedCells ].forEach(function(n) { - o.write_shift(4, 1); - if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); - else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + if(n[1]) o.write_shift(4, sp[n[0]] != null && !sp[n[0]] ? 1 : 0); + else o.write_shift(4, sp[n[0]] != null && sp[n[0]] ? 0 : 1); }); return o; } @@ -10813,6 +10929,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { if(opts.sheetRows && opts.sheetRows <= row.r) end=true; rr = encode_row(R = row.r); opts['!row'] = row.r; + if(val.hidden || val.hpt) { + if(val.hpt) val.hpx = pt2px(val.hpt); + rowinfo[val.r] = val; + } break; case 0x0002: /* 'BrtCellRk' */ @@ -10910,7 +11030,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { case 0x003C: /* 'BrtColInfo' */ if(!opts.cellStyles) break; while(val.e >= val.s) { - colinfo[val.e--] = { width: val.w/256 }; + colinfo[val.e--] = { width: val.w/256, hidden: !!(val.flags & 0x01) }; if(!seencol) { seencol = true; find_mdw_colw(val.w/256); } process_col(colinfo[val.e+1]); } @@ -11126,6 +11246,21 @@ function write_AUTOFILTER(ba, ws) { write_record(ba, "BrtEndAFilter"); } +function write_WSVIEWS2(ba, ws) { + write_record(ba, "BrtBeginWsViews"); + { /* 1*WSVIEW2 */ + /* [ACUID] */ + write_record(ba, "BrtBeginWsView", write_BrtBeginWsView(ws)); + /* [BrtPane] */ + /* *4BrtSel */ + /* *4SXSELECT */ + /* *FRT */ + write_record(ba, "BrtEndWsView"); + } + /* *FRT */ + write_record(ba, "BrtEndWsViews"); +} + function write_SHEETPROTECT(ba, ws) { if(!ws['!protect']) return; /* [BrtSheetProtectionIso] */ @@ -11142,7 +11277,7 @@ function write_ws_bin(idx, opts, wb, rels) { write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); - /* [WSVIEWS2] */ + write_WSVIEWS2(ba, ws); /* [WSFMTINFO] */ write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); @@ -12160,7 +12295,7 @@ function parse_xlml_xml(d, opts) { var comments = [], comment = {}; var cstys = [], csty, seencol = false; var arrayf = []; - var rowinfo = []; + var rowinfo = [], rowobj = {}; var Workbook = { Sheets:[] }, wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); @@ -12224,6 +12359,12 @@ for(var cma = c; cma <= cc; ++cma) { } else { row = xlml_parsexmltag(Rn[0]); if(row.Index) r = +row.Index - 1; + rowobj = {}; + if(row.AutoFitHeight == "0") { + rowobj.hpx = parseInt(row.Height, 10); rowobj.hpt = px2pt(rowobj.hpx); + rowinfo[r] = rowobj; + } + if(row.Hidden == "1") { rowobj.hidden = true; rowinfo[r] = rowobj; } } break; case 'Worksheet': /* TODO: read range from FullRows/FullColumns */ @@ -12274,9 +12415,10 @@ for(var cma = c; cma <= cc; ++cma) { case 'Column': if(state[state.length-1][0] !== 'Table') break; csty = xlml_parsexmltag(Rn[0]); - csty.wpx = parseInt(csty.Width, 10); + if(csty.Hidden) { csty.hidden = true; delete csty.Hidden; } + if(csty.Width) csty.wpx = parseInt(csty.Width, 10); if(!seencol && csty.wpx > 10) { - seencol = true; find_mdw_wpx(csty.wpx); + seencol = true; MDW = DEF_MDW; //find_mdw_wpx(csty.wpx); for(var _col = 0; _col < cstys.length; ++_col) if(cstys[_col]) process_col(cstys[_col]); } if(seencol) process_col(csty); @@ -12413,7 +12555,7 @@ for(var cma = c; cma <= cc; ++cma) { case 'Color': break; case 'Index': break; case 'RGB': break; - case 'PixelsPerInch': break; + case 'PixelsPerInch': break; // TODO: set PPI case 'TargetScreenSize': break; case 'ReadOnlyRecommended': break; default: seen = false; @@ -12964,6 +13106,15 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){ return writextag("Cell", m, attr); } +function write_ws_xlml_row(R, row) { + var o = ''; +} /* TODO */ function write_ws_xlml_table(ws, opts, idx, wb) { if(!ws['!ref']) return ""; @@ -12971,12 +13122,17 @@ function write_ws_xlml_table(ws, opts, idx, wb) { var marr = ws['!merges'] || [], mi = 0; var o = []; if(ws['!cols']) ws['!cols'].forEach(function(n, i) { + process_col(n); + var w = !!n.width; var p = col_obj_w(i, n); - o.push(writextag("Column",null, {"ss:Index":i+1, "ss:Width":width2px(p.width)})); + var k = {"ss:Index":i+1}; + if(w) k['ss:Width'] = width2px(p.width); + if(n.hidden) k['ss:Hidden']="1"; + o.push(writextag("Column",null,k)); }); var dense = Array.isArray(ws); for(var R = range.s.r; R <= range.e.r; ++R) { - var row = ['']; + var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])]; for(var C = range.s.c; C <= range.e.c; ++C) { var skip = false; for(mi = 0; mi != marr.length; ++mi) { @@ -13531,7 +13687,14 @@ function parse_workbook(blob, options) { process_col(colinfo[val.e+1]); } } break; - case 'Row': break; // TODO + case 'Row': { + var rowobj = {}; + if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } + if(val.hpt) { + rowinfo[val.r] = rowobj; + rowobj.hpt = val.hpt; rowobj.hpx = pt2px(val.hpt); + } + } break; case 'LeftMargin': case 'RightMargin':