From 1587688aea57de985bb04e84aedbd992a73e177c Mon Sep 17 00:00:00 2001 From: SheetJS Date: Wed, 12 Apr 2017 21:29:38 -0400 Subject: [PATCH] Page Margins - XLSB read/write page margins - XLSX/XLS/XLML read page margins - separated encrypted XLSX/XLSB document logic from XLS --- README.md | 22 +++ bits/23_binutils.js | 12 ++ bits/39_xlsbiff.js | 12 +- bits/44_offcrypto.js | 140 +++++++++++++++++-- bits/66_wscommon.js | 12 ++ bits/67_wsxml.js | 13 ++ bits/68_wsbin.js | 30 +++- bits/75_xlml.js | 19 ++- bits/76_xls.js | 22 ++- bits/77_parsetab.js | 2 +- bits/85_parsezip.js | 38 +++++ bits/87_read.js | 7 +- docbits/53_wsobject.md | 22 +++ test.js | 118 ++++++++++++---- test_files | 2 +- tests/write.js | 3 + xlsx.flow.js | 307 +++++++++++++++++++++++++++++++++++++---- xlsx.js | 307 +++++++++++++++++++++++++++++++++++++---- 18 files changed, 987 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index ad52de7..430ce16 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,28 @@ Special sheet keys (accessible as `sheet[key]`, each starting with `!`): When reading a worksheet with the `sheetRows` property set, the ref parameter will use the restricted range. The original range is set at `ws['!fullref']` +- `sheet['!margins']`: Object representing the page margins. The default values + follow Excel's "normal" preset. Excel also has a "wide" and a "narrow" preset + but they are stored as raw measurements. The main properties are listed below: + +| key | description | "normal" | "wide" | "narrow" | +|----------|------------------------|:---------|:-------|:-------- | +| `left` | left margin (inches) | `0.7` | `1.0` | `0.25` | +| `right` | right margin (inches) | `0.7` | `1.0` | `0.25` | +| `top` | top margin (inches) | `0.75` | `1.0` | `0.75` | +| `bottom` | bottom margin (inches) | `0.75` | `1.0` | `0.75` | +| `header` | header margin (inches) | `0.3` | `0.5` | `0.3` | +| `footer` | footer margin (inches) | `0.3` | `0.5` | `0.3` | + +```js +/* Set worksheet sheet to "normal" */ +sheet["!margins"] = { left:0.7, right:0.7, top:0.75, bottom:0.75, header:0.3, footer:0.3 } +/* Set worksheet sheet to "wide" */ +sheet["!margins"] = { left:1.0, right:1.0, top:1.0, bottom:1.0, header:0.5, footer:0.5 } +/* Set worksheet sheet to "narrow" */ +sheet["!margins"] = { left:0.25, right:0.25, top:0.75, bottom:0.75, header:0.3, footer:0.3 } +``` + #### Worksheet Object In addition to the base sheet keys, worksheets also add: diff --git a/bits/23_binutils.js b/bits/23_binutils.js index 25a9daf..7feaa67 100644 --- a/bits/23_binutils.js +++ b/bits/23_binutils.js @@ -36,6 +36,10 @@ var __lpstr, ___lpstr; __lpstr = ___lpstr = function lpstr_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; var __lpwstr, ___lpwstr; __lpwstr = ___lpwstr = function lpwstr_(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; +var __lpp4, ___lpp4; +__lpp4 = ___lpp4 = function lpp4_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf16le(b, i+4,i+4+len) : "";}; +var __8lpp4, ___8lpp4; +__8lpp4 = ___8lpp4 = function lpp4_8(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len) : "";}; var __double, ___double; __double = ___double = function(b, idx) { return read_double_le(b, idx);}; @@ -45,6 +49,8 @@ if(has_buf/*:: && typeof Buffer != 'undefined'*/) { __hexlify = function(b,s,l) { return Buffer.isBuffer(b) ? b.toString('hex',s,s+l) : ___hexlify(b,s,l); }; __lpstr = function lpstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpstr(b, i); var len = b.readUInt32LE(i); return len > 0 ? b.toString('utf8',i+4,i+4+len-1) : "";}; __lpwstr = function lpwstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpwstr(b, i); var len = 2*b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len-1);}; + __lpp4 = function lpp4_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len);}; + __8lpp4 = function lpp4_8b(b,i) { if(!Buffer.isBuffer(b)) return ___8lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf8',i+4,i+4+len);}; __utf8 = function utf8_b(b, s,e) { return b.toString('utf8',s,e); }; __toBuffer = function(bufs) { return (bufs[0].length > 0 && Buffer.isBuffer(bufs[0][0])) ? Buffer.concat(bufs[0]) : ___toBuffer(bufs);}; bconcat = function(bufs) { return Buffer.isBuffer(bufs[0]) ? Buffer.concat(bufs) : [].concat.apply([], bufs); }; @@ -58,6 +64,8 @@ if(typeof cptable !== 'undefined') { __utf8 = function(b,s,e) { return cptable.utils.decode(65001, b.slice(s,e)); }; __lpstr = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(current_codepage, b.slice(i+4, i+4+len-1)) : "";}; __lpwstr = function(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len-1)) : "";}; + __lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len)) : "";}; + __8lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(65001, b.slice(i+4,i+4+len)) : "";}; } var __readUInt8 = function(b, idx) { return b[idx]; }; @@ -91,6 +99,10 @@ function ReadShift(size/*:number*/, t/*:?string*/) { case 'lpstr': o = __lpstr(this, this.l); size = 5 + o.length; break; /* [MS-OLEDS] 2.1.5 LengthPrefixedUnicodeString */ case 'lpwstr': o = __lpwstr(this, this.l); size = 5 + o.length; if(o[o.length-1] == '\u0000') size += 2; break; + /* [MS-OFFCRYPTO] 2.1.2 Length-Prefixed Padded Unicode String (UNICODE-LP-P4) */ + case 'lpp4': size = 4 + __readUInt32LE(this, this.l); o = __lpp4(this, this.l); if(size & 0x02) size += 2; break; + /* [MS-OFFCRYPTO] 2.1.3 Length-Prefixed UTF-8 String (UTF-8-LP-P4) */ + case '8lpp4': size = 4 + __readUInt32LE(this, this.l); o = __8lpp4(this, this.l); if(size & 0x03) size += 4 - (size & 0x03); break; case 'cstr': size = 0; o = ""; while((w=__readUInt8(this, this.l + size++))!==0) oo.push(_getchar(w)); diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js index eba93a7..d0b2d64 100644 --- a/bits/39_xlsbiff.js +++ b/bits/39_xlsbiff.js @@ -655,6 +655,16 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.257 */ +function parse_Setup(blob, length, opts) { + var o = {}; + blob.l += 16; + o.header = parse_Xnum(blob, 8); + o.footer = parse_Xnum(blob, 8); + blob.l += 2; + return o; +} + /* 2.4.261 */ function parse_ShtProps(blob, length, opts) { var def = {area:false}; @@ -664,7 +674,6 @@ function parse_ShtProps(blob, length, opts) { return def; } - var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -747,7 +756,6 @@ var parse_FnGroupName = parsenoop; var parse_FilterMode = parsenoop; var parse_AutoFilterInfo = parsenoop; var parse_AutoFilter = parsenoop; -var parse_Setup = parsenoop; var parse_ScenMan = parsenoop; var parse_SCENARIO = parsenoop; var parse_SxView = parsenoop; diff --git a/bits/44_offcrypto.js b/bits/44_offcrypto.js index fb3d4be..3b3696b 100644 --- a/bits/44_offcrypto.js +++ b/bits/44_offcrypto.js @@ -6,38 +6,154 @@ function _JS2ANSI(str/*:string*/)/*:Array*/ { } /* [MS-OFFCRYPTO] 2.1.4 Version */ -function parse_Version(blob, length/*:number*/) { +function parse_CRYPTOVersion(blob, length/*:number*/) { var o = {}; o.Major = blob.read_shift(2); o.Minor = blob.read_shift(2); return o; } + +/* [MS-OFFCRYPTO] 2.1.5 DataSpaceVersionInfo */ +function parse_DataSpaceVersionInfo(blob, length) { + var o = {}; + o.id = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6.1 DataSpaceMapEntry Structure */ +function parse_DataSpaceMapEntry(blob) { + var len = blob.read_shift(4); + var end = blob.l + len - 4; + var o = {}; + var cnt = blob.read_shift(4); + var comps = []; + while(cnt-- > 0) { + /* [MS-OFFCRYPTO] 2.1.6.2 DataSpaceReferenceComponent Structure */ + var rc = {}; + rc.t = blob.read_shift(4); + rc.v = blob.read_shift(0, 'lpp4'); + comps.push(rc); + } + o.name = blob.read_shift(0, 'lpp4'); + o.comps = comps; + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6 DataSpaceMap */ +function parse_DataSpaceMap(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(parse_DataSpaceMapEntry(blob)); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.7 DataSpaceDefinition */ +function parse_DataSpaceDefinition(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(blob.read_shift(0, 'lpp4')); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.8 DataSpaceDefinition */ +function parse_TransformInfoHeader(blob, length) { + var o = {}; + var len = blob.read_shift(4); + var tgt = blob.l + len - 4; + blob.l += 4; // must be 0x1 + o.id = blob.read_shift(0, 'lpp4'); + // tgt == len + o.name = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +function parse_Primary(blob, length) { + /* [MS-OFFCRYPTO] 2.2.6 IRMDSTransformInfo */ + var hdr = parse_TransformInfoHeader(blob); + /* [MS-OFFCRYPTO] 2.1.9 EncryptionTransformInfo */ + hdr.ename = blob.read_shift(0, '8lpp4'); + hdr.blksz = blob.read_shift(4); + hdr.cmode = blob.read_shift(4); + if(blob.read_shift(4) != 0x04) throw new Error("Bad !Primary record"); + return hdr; +} + /* [MS-OFFCRYPTO] 2.3.2 Encryption Header */ function parse_EncryptionHeader(blob, length/*:number*/) { + var tgt = blob.l + length; var o = {}; - o.Flags = blob.read_shift(4); - - // Check if SizeExtra is 0x00000000 - var tmp = blob.read_shift(4); - if(tmp !== 0) throw 'Unrecognized SizeExtra: ' + tmp; - + o.Flags = (blob.read_shift(4) & 0x3F); + blob.l += 4; o.AlgID = blob.read_shift(4); + var valid = false; switch(o.AlgID) { - case 0: case 0x6801: case 0x660E: case 0x660F: case 0x6610: break; + case 0x660E: case 0x660F: case 0x6610: valid = (o.Flags == 0x24); break; + case 0x6801: valid = (o.Flags == 0x04); break; + case 0: valid = (o.Flags == 0x10 || o.Flags == 0x04 || o.Flags == 0x24); break; default: throw 'Unrecognized encryption algorithm: ' + o.AlgID; } - parsenoop(blob, length-12); + if(!valid) throw new Error("Encryption Flags/AlgID mismatch"); + o.AlgIDHash = blob.read_shift(4); + o.KeySize = blob.read_shift(4); + o.ProviderType = blob.read_shift(4); + blob.l += 8; + o.CSPName = blob.read_shift((tgt-blob.l)>>1, 'utf16le').slice(0,-1); + blob.l = tgt; return o; } /* [MS-OFFCRYPTO] 2.3.3 Encryption Verifier */ function parse_EncryptionVerifier(blob, length/*:number*/) { - return parsenoop(blob, length); + var o = {}; + blob.l += 4; // SaltSize must be 0x10 + o.Salt = blob.slice(blob.l, blob.l+16); blob.l += 16; + o.Verifier = blob.slice(blob.l, blob.l+16); blob.l += 16; + var sz = blob.read_shift(4); + o.VerifierHash = blob.slice(blob.l, blob.l + sz); blob.l += sz; + return o; } + +/* [MS-OFFCRYPTO] 2.3.4.* EncryptionInfo Stream */ +function parse_EncryptionInfo(blob, length) { + var vers = parse_CRYPTOVersion(blob); + switch(vers.Minor) { + case 0x02: return parse_EncInfoStd(blob, vers); + case 0x03: return parse_EncInfoExt(blob, vers); + case 0x04: return parse_EncInfoAgl(blob, vers); + } + throw new Error("ECMA-376 Encryped file unrecognized Version: " + vers.Minor); +} + +/* [MS-OFFCRYPTO] 2.3.4.5 EncryptionInfo Stream (Standard Encryption) */ +function parse_EncInfoStd(blob, vers) { + var flags = blob.read_shift(4); + if((flags & 0x3F) != 0x24) throw new Error("EncryptionInfo mismatch"); + var sz = blob.read_shift(4); + var tgt = blob.l + sz; + var hdr = parse_EncryptionHeader(blob, sz); + var verifier = parse_EncryptionVerifier(blob, blob.length - blob.l); + return { t:"Std", h:hdr, v:verifier }; +} +/* [MS-OFFCRYPTO] 2.3.4.6 EncryptionInfo Stream (Extensible Encryption) */ +function parse_EncInfoExt(blob, vers) { throw new Error("File is password-protected: ECMA-376 Extensible"); } +/* [MS-OFFCRYPTO] 2.3.4.10 EncryptionInfo Stream (Agile Encryption) */ +function parse_EncInfoAgl(blob, vers) { throw new Error("File is password-protected: ECMA-376 Agile"); } + + + + /* [MS-OFFCRYPTO] 2.3.5.1 RC4 CryptoAPI Encryption Header */ function parse_RC4CryptoHeader(blob, length/*:number*/) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Minor != 2) throw 'unrecognized minor version code: ' + vers.Minor; if(vers.Major > 4 || vers.Major < 2) throw 'unrecognized major version code: ' + vers.Major; o.Flags = blob.read_shift(4); length -= 4; @@ -49,7 +165,7 @@ function parse_RC4CryptoHeader(blob, length/*:number*/) { /* [MS-OFFCRYPTO] 2.3.6.1 RC4 Encryption Header */ function parse_RC4Header(blob, length/*:number*/) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Major != 1 || vers.Minor != 1) throw 'unrecognized version code ' + vers.Major + ' : ' + vers.Minor; o.Salt = blob.read_shift(16); o.EncryptedVerifier = blob.read_shift(16); diff --git a/bits/66_wscommon.js b/bits/66_wscommon.js index 7a26a03..5651a56 100644 --- a/bits/66_wscommon.js +++ b/bits/66_wscommon.js @@ -24,6 +24,18 @@ function col_obj_w(C/*:number*/, col) { return p; } +function default_margins(margins, mode) { + if(!margins) return; + var defs = [0.7, 0.7, 0.75, 0.75, 0.3, 0.3]; + if(mode == 'xlml') defs = [1, 1, 1, 1, 0.5, 0.5]; + if(margins.left == null) margins.left = defs[0]; + if(margins.right == null) margins.right = defs[1]; + if(margins.top == null) margins.top = defs[2]; + if(margins.bottom == null) margins.bottom = defs[3]; + if(margins.header == null) margins.header = defs[4]; + if(margins.footer == null) margins.footer = defs[5]; +} + function get_cell_style(styles, cell, opts) { var z = opts.revssf[cell.z != null ? cell.z : "General"]; for(var i = 0, len = styles.length; i != len; ++i) if(styles[i].numFmtId === z) return i; diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 37fba84..e25d10b 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -8,6 +8,7 @@ var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; +var marginregex= /<(?:\w:)?pageMargins[^>]*\/>/g; /* 18.3 Worksheets */ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; @@ -57,6 +58,10 @@ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksh var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); + /* 18.3.1.62 pageMargins CT_PageMargins */ + var margins = data2.match(marginregex); + if(margins) s['!margins'] = parse_ws_xml_margins(parsexmltag(margins[0])); + if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); if(opts.sheetRows > 0 && s["!ref"]) { var tmpref = safe_decode_range(s["!ref"]); @@ -131,6 +136,14 @@ function parse_ws_xml_hlinks(s, data/*:Array*/, rels) { } } +function parse_ws_xml_margins(margin) { + var o = {}; + ["left", "right", "top", "bottom", "header", "footer"].forEach(function(k) { + if(margin[k]) o[k] = parseFloat(margin[k]); + }); + return o; +} + function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index 7cd97b3..b073512 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -289,6 +289,29 @@ function write_BrtColInfo(C/*:number*/, col, o) { return o; } +/* [MS-XLSB] 2.4.672 BrtMargins */ +function parse_BrtMargins(data, length, opts) { + return { + left: parse_Xnum(data, 8), + right: parse_Xnum(data, 8), + top: parse_Xnum(data, 8), + bottom: parse_Xnum(data, 8), + header: parse_Xnum(data, 8), + footer: parse_Xnum(data, 8) + }; +} +function write_BrtMargins(margins, o) { + if(o == null) o = new_buf(6*8); + default_margins(margins); + write_Xnum(margins.left, o); + write_Xnum(margins.right, o); + write_Xnum(margins.top, o); + write_Xnum(margins.bottom, o); + write_Xnum(margins.header, o); + write_Xnum(margins.footer, o); + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -467,6 +490,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { s['!autofilter'] = { ref:encode_range(val) }; break; + case 0x01DC: /* 'BrtMargins' */ + s['!margins'] = val; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -498,7 +525,6 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { case 0x0227: /* 'BrtLegacyDrawing' */ case 0x0228: /* 'BrtLegacyDrawingHF' */ case 0x0295: /* 'BrtListPart' */ - case 0x01DC: /* 'BrtMargins' */ case 0x027F: /* 'BrtOleObject' */ case 0x01DE: /* 'BrtPageSetup' */ case 0x0097: /* 'BrtPane' */ @@ -704,7 +730,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { /* [DVALS] */ write_HLINKS(ba, ws, rels); /* [BrtPrintOptions] */ - /* [BrtMargins] */ + if(ws['!margins']) write_record(ba, "BrtMargins", write_BrtMargins(ws['!margins'])); /* [BrtPageSetup] */ /* [HEADERFOOTER] */ /* [RWBRK] */ diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 1c995b5..eef9ed5 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -528,6 +528,22 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } else pidx = Rn.index + Rn[0].length; break; + case 'Header': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].header = parsexmltag(Rn[0]).Margin; + break; + case 'Footer': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].footer = parsexmltag(Rn[0]).Margin; + break; + case 'PageMargins': + var pagemargins = parsexmltag(Rn[0]); + if(!cursheet['!margins']) default_margins(cursheet['!margins']={},'xlml'); + if(pagemargins.Top) cursheet['!margins'].top = pagemargins.Top; + if(pagemargins.Left) cursheet['!margins'].left = pagemargins.Left; + if(pagemargins.Right) cursheet['!margins'].right = pagemargins.Right; + if(pagemargins.Bottom) cursheet['!margins'].bottom = pagemargins.Bottom; + break; case 'Unsynced': break; case 'Print': break; case 'Panes': break; @@ -535,10 +551,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Pane': break; case 'Number': break; case 'Layout': break; - case 'Header': break; - case 'Footer': break; case 'PageSetup': break; - case 'PageMargins': break; case 'Selected': break; case 'ProtectObjects': break; case 'EnableSelection': break; diff --git a/bits/76_xls.js b/bits/76_xls.js index 9814d37..476f084 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -503,12 +503,30 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': break; // TODO + case 'LeftMargin': + case 'RightMargin': + case 'TopMargin': + case 'BottomMargin': + if(!out['!margins']) default_margins(out['!margins'] = {}); + switch(Rn) { + case 'LeftMargin': out['!margins'].left = val; break; + case 'RightMargin': out['!margins'].right = val; break; + case 'TopMargin': out['!margins'].top = val; break; + case 'BottomMargin': out['!margins'].bottom = val; break; + } + break; + + case 'Setup': // TODO + if(!out['!margins']) default_margins(out['!margins'] = {}); + out['!margins'].header = val.header; + out['!margins'].footer = val.footer; + break; + case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO case 'VCenter': break; // TODO case 'Pls': break; // TODO - case 'Setup': break; // TODO case 'GCW': break; case 'LHRecord': break; case 'DBCell': break; // TODO @@ -703,7 +721,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'WebPub': case 'AutoWebPub': /* Print Stuff */ - case 'RightMargin': case 'LeftMargin': case 'TopMargin': case 'BottomMargin': case 'HeaderFooter': case 'HFPicture': case 'PLV': case 'HorizontalPageBreaks': case 'VerticalPageBreaks': /* Behavioral */ @@ -756,7 +773,6 @@ fix_read_opts(options); reset_cp(); var CompObj, Summary, Workbook/*:?any*/; if(cfb.FullPaths) { - if(cfb.find("EncryptedPackage")) throw new Error("File is password-protected"); CompObj = cfb.find('!CompObj'); Summary = cfb.find('!SummaryInformation'); Workbook = cfb.find('/Workbook'); diff --git a/bits/77_parsetab.js b/bits/77_parsetab.js index 977e3d0..05a188c 100644 --- a/bits/77_parsetab.js +++ b/bits/77_parsetab.js @@ -406,7 +406,7 @@ var XLSBRecordEnum = { /*::[*/0x01D9/*::]*/: { n:"BrtBeginColorPalette", f:parsenoop }, /*::[*/0x01DA/*::]*/: { n:"BrtEndColorPalette", f:parsenoop }, /*::[*/0x01DB/*::]*/: { n:"BrtIndexedColor", f:parsenoop }, - /*::[*/0x01DC/*::]*/: { n:"BrtMargins", f:parsenoop }, + /*::[*/0x01DC/*::]*/: { n:"BrtMargins", f:parse_BrtMargins }, /*::[*/0x01DD/*::]*/: { n:"BrtPrintOptions", f:parsenoop }, /*::[*/0x01DE/*::]*/: { n:"BrtPageSetup", f:parsenoop }, /*::[*/0x01DF/*::]*/: { n:"BrtBeginHeaderFooter", f:parsenoop }, diff --git a/bits/85_parsezip.js b/bits/85_parsezip.js index 7e34d4f..10910e3 100644 --- a/bits/85_parsezip.js +++ b/bits/85_parsezip.js @@ -168,3 +168,41 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { } return out; } + +/* references to [MS-OFFCRYPTO] */ +function parse_xlsxcfb(cfb, opts/*:?ParseOpts*/)/*:Workbook*/ { + var f = 'Version'; + var data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var version = parse_DataSpaceVersionInfo(data.content); + + /* 2.3.4.1 */ + f = 'DataSpaceMap'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var dsm = parse_DataSpaceMap(data.content); + if(dsm.length != 1 || dsm[0].comps.length != 1 || dsm[0].comps[0].t != 0 || + dsm[0].name != "StrongEncryptionDataSpace" || dsm[0].comps[0].v != "EncryptedPackage") + throw new Error("ECMA-376 Encrypted file bad " + f); + + f = 'StrongEncryptionDataSpace'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var seds = parse_DataSpaceDefinition(data.content); + if(seds.length != 1 || seds[0] != "StrongEncryptionTransform") + throw new Error("ECMA-376 Encrypted file bad " + f); + + /* 2.3.4.3 */ + f = '!Primary'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var hdr = parse_Primary(data.content); + + f = 'EncryptionInfo'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var einfo = parse_EncryptionInfo(data.content); + + throw new Error("File is password-protected"); +} + diff --git a/bits/87_read.js b/bits/87_read.js index b323921..adc0c55 100644 --- a/bits/87_read.js +++ b/bits/87_read.js @@ -10,6 +10,11 @@ function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:Array*/ { return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } +function read_cfb(cfb, opts/*:?ParseOpts*/)/*:Workbook*/ { + if(cfb.find("EncryptedPackage")) return parse_xlsxcfb(cfb, opts); + return parse_xlscfb(cfb, opts); +} + function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { /*:: if(!jszip) throw new Error("JSZip is not available"); */ var zip, d = data; @@ -39,7 +44,7 @@ function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } switch((n = firstbyte(d, o))[0]) { - case 0xD0: return parse_xlscfb(CFB.read(d, o), o); + case 0xD0: return read_cfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); case 0x49: if(n[1] == 0x44) return SYLK.to_workbook(d, o); break; diff --git a/docbits/53_wsobject.md b/docbits/53_wsobject.md index 777d747..f89dc0b 100644 --- a/docbits/53_wsobject.md +++ b/docbits/53_wsobject.md @@ -20,3 +20,25 @@ Special sheet keys (accessible as `sheet[key]`, each starting with `!`): When reading a worksheet with the `sheetRows` property set, the ref parameter will use the restricted range. The original range is set at `ws['!fullref']` +- `sheet['!margins']`: Object representing the page margins. The default values + follow Excel's "normal" preset. Excel also has a "wide" and a "narrow" preset + but they are stored as raw measurements. The main properties are listed below: + +| key | description | "normal" | "wide" | "narrow" | +|----------|------------------------|:---------|:-------|:-------- | +| `left` | left margin (inches) | `0.7` | `1.0` | `0.25` | +| `right` | right margin (inches) | `0.7` | `1.0` | `0.25` | +| `top` | top margin (inches) | `0.75` | `1.0` | `0.75` | +| `bottom` | bottom margin (inches) | `0.75` | `1.0` | `0.75` | +| `header` | header margin (inches) | `0.3` | `0.5` | `0.3` | +| `footer` | footer margin (inches) | `0.3` | `0.5` | `0.3` | + +```js +/* Set worksheet sheet to "normal" */ +sheet["!margins"] = { left:0.7, right:0.7, top:0.75, bottom:0.75, header:0.3, footer:0.3 } +/* Set worksheet sheet to "wide" */ +sheet["!margins"] = { left:1.0, right:1.0, top:1.0, bottom:1.0, header:0.5, footer:0.5 } +/* Set worksheet sheet to "narrow" */ +sheet["!margins"] = { left:0.25, right:0.25, top:0.75, bottom:0.75, header:0.3, footer:0.3 } +``` + diff --git a/test.js b/test.js index 3a904da..afca66d 100644 --- a/test.js +++ b/test.js @@ -37,57 +37,76 @@ var paths = { afods: dir + 'AutoFilter.ods', afxlsx: dir + 'AutoFilter.xlsx', afxlsb: dir + 'AutoFilter.xlsb', + cpxls: dir + 'custom_properties.xls', cpxml: dir + 'custom_properties.xls.xml', cpxlsx: dir + 'custom_properties.xlsx', cpxlsb: dir + 'custom_properties.xlsb', + cssxls: dir + 'cell_style_simple.xls', cssxml: dir + 'cell_style_simple.xml', cssxlsx: dir + 'cell_style_simple.xlsx', cssxlsb: dir + 'cell_style_simple.xlsb', + cstxls: dir + 'comments_stress_test.xls', cstxml: dir + 'comments_stress_test.xls.xml', cstxlsx: dir + 'comments_stress_test.xlsx', cstxlsb: dir + 'comments_stress_test.xlsb', cstods: dir + 'comments_stress_test.ods', - dnsxls: dir + 'defined_names_simple.xls', - dnsxml: dir + 'defined_names_simple.xml', - dnsxlsx: dir + 'defined_names_simple.xlsx', - dnsxlsb: dir + 'defined_names_simple.xlsb', - fstxls: dir + 'formula_stress_test.xls', - fstxml: dir + 'formula_stress_test.xls.xml', - fstxlsx: dir + 'formula_stress_test.xlsx', - fstxlsb: dir + 'formula_stress_test.xlsb', - fstods: dir + 'formula_stress_test.ods', - hlxls: dir + 'hyperlink_stress_test_2011.xls', - hlxml: dir + 'hyperlink_stress_test_2011.xml', - hlxlsx: dir + 'hyperlink_stress_test_2011.xlsx', - hlxlsb: dir + 'hyperlink_stress_test_2011.xlsb', - lonxls: dir + 'LONumbers.xls', - lonxlsx: dir + 'LONumbers.xlsx', - mcxls: dir + 'merge_cells.xls', - mcxml: dir + 'merge_cells.xls.xml', - mcxlsx: dir + 'merge_cells.xlsx', - mcxlsb: dir + 'merge_cells.xlsb', - mcods: dir + 'merge_cells.ods', - nfxls: dir + 'number_format.xls', - nfxml: dir + 'number_format.xls.xml', - nfxlsx: dir + 'number_format.xlsm', - nfxlsb: dir + 'number_format.xlsb', - dtxls: dir + 'xlsx-stream-d-date-cell.xls', - dtxml: dir + 'xlsx-stream-d-date-cell.xls.xml', - dtxlsx: dir + 'xlsx-stream-d-date-cell.xlsx', - dtxlsb: dir + 'xlsx-stream-d-date-cell.xlsb', + cwxls: dir + 'column_width.xlsx', cwxls5: dir + 'column_width.biff5', cwxml: dir + 'column_width.xml', cwxlsx: dir + 'column_width.xlsx', cwxlsb: dir + 'column_width.xlsx', + + dnsxls: dir + 'defined_names_simple.xls', + dnsxml: dir + 'defined_names_simple.xml', + dnsxlsx: dir + 'defined_names_simple.xlsx', + dnsxlsb: dir + 'defined_names_simple.xlsb', + + dtxls: dir + 'xlsx-stream-d-date-cell.xls', + dtxml: dir + 'xlsx-stream-d-date-cell.xls.xml', + dtxlsx: dir + 'xlsx-stream-d-date-cell.xlsx', + dtxlsb: dir + 'xlsx-stream-d-date-cell.xlsb', + + fstxls: dir + 'formula_stress_test.xls', + fstxml: dir + 'formula_stress_test.xls.xml', + fstxlsx: dir + 'formula_stress_test.xlsx', + fstxlsb: dir + 'formula_stress_test.xlsb', + fstods: dir + 'formula_stress_test.ods', + + hlxls: dir + 'hyperlink_stress_test_2011.xls', + hlxml: dir + 'hyperlink_stress_test_2011.xml', + hlxlsx: dir + 'hyperlink_stress_test_2011.xlsx', + hlxlsb: dir + 'hyperlink_stress_test_2011.xlsb', + + lonxls: dir + 'LONumbers.xls', + lonxlsx: dir + 'LONumbers.xlsx', + + mcxls: dir + 'merge_cells.xls', + mcxml: dir + 'merge_cells.xls.xml', + mcxlsx: dir + 'merge_cells.xlsx', + mcxlsb: dir + 'merge_cells.xlsb', + mcods: dir + 'merge_cells.ods', + + nfxls: dir + 'number_format.xls', + nfxml: dir + 'number_format.xls.xml', + nfxlsx: dir + 'number_format.xlsm', + nfxlsb: dir + 'number_format.xlsb', + + pmxls: dir + 'page_margins_2016.xls', + pmxls5: dir + 'page_margins_2016_5.xls', + pmxml: dir + 'page_margins_2016.xml', + pmxlsx: dir + 'page_margins_2016.xlsx', + pmxlsb: dir + 'page_margins_2016.xlsb', + svxls: dir + 'sheet_visibility.xls', svxls5: dir + 'sheet_visibility.xls', svxml: dir + 'sheet_visibility.xml', svxlsx: dir + 'sheet_visibility.xlsx', svxlsb: dir + 'sheet_visibility.xlsb', + swcxls: dir + 'apachepoi_SimpleWithComments.xls', swcxml: dir + '2011/apachepoi_SimpleWithComments.xls.xml', swcxlsx: dir + 'apachepoi_SimpleWithComments.xlsx', @@ -937,6 +956,49 @@ describe('parse features', function() { }); }); }); + describe('page margins', function() { + function check_margin(margins, exp) { + assert.equal(margins.left, exp[0]); + assert.equal(margins.right, exp[1]); + assert.equal(margins.top, exp[2]); + assert.equal(margins.bottom, exp[3]); + assert.equal(margins.header, exp[4]); + assert.equal(margins.footer, exp[5]); + } + var wb1, wb2, wb3, wb4, wb5, wbs; + var bef = (function() { + wb1 = X.readFile(paths.pmxls); + wb2 = X.readFile(paths.pmxls5); + wb3 = X.readFile(paths.pmxml); + wb4 = X.readFile(paths.pmxlsx); + wb5 = X.readFile(paths.pmxlsb); + wbs = [wb1, wb2, wb3, wb4, wb5]; + }); + if(typeof before != 'undefined') before(bef); + else it('before', bef); + it('should parse normal margin', function() { + wbs.forEach(function(wb) { + check_margin(wb.Sheets["Normal"]["!margins"], [0.7, 0.7, 0.75, 0.75, 0.3, 0.3]); + }); + }); + it('should parse wide margins ', function() { + wbs.forEach(function(wb) { + check_margin(wb.Sheets["Wide"]["!margins"], [1, 1, 1, 1, 0.5, 0.5]); + }); + }); + it('should parse narrow margins ', function() { + wbs.forEach(function(wb) { + check_margin(wb.Sheets["Narrow"]["!margins"], [0.25, 0.25, 0.75, 0.75, 0.3, 0.3]); + }); + }); + it('should parse custom margins ', function() { + wbs.forEach(function(wb) { + check_margin(wb.Sheets["Custom 1 Inch Centered"]["!margins"], [1, 1, 1, 1, 0.3, 0.3]); + check_margin(wb.Sheets["1 Inch HF"]["!margins"], [0.7, 0.7, 0.75, 0.75, 1, 1]); + }); + }); + }); + describe('should correctly handle styles', function() { var wsxls, wsxlsx, rn, rn2; var bef = (function() { diff --git a/test_files b/test_files index 6a9e589..249b005 160000 --- a/test_files +++ b/test_files @@ -1 +1 @@ -Subproject commit 6a9e5891f206ca9c5ff83489165548a105e5fd29 +Subproject commit 249b005fddf7cea0b2c2d1aff5a2414e47e70c0e diff --git a/tests/write.js b/tests/write.js index ef5f9bf..105df79 100644 --- a/tests/write.js +++ b/tests/write.js @@ -76,6 +76,9 @@ ws['B1'].z = "0%"; // Format Code 9 /* TEST: custom format */ //ws['B2'].z = "0.0"; wb.SSF[60] = "0.0"; // Custom +/* TEST: page margins */ +ws['!margins'] = { left:1.0, right:1.0, top:1.0, bottom:1.0, header:0.5, footer:0.5 }; + console.log("JSON Data:");console.log(XLSX.utils.sheet_to_json(ws, {header:1})); /* TEST: hidden sheets */ diff --git a/xlsx.flow.js b/xlsx.flow.js index 3272cba..8f5a4d7 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -1780,6 +1780,10 @@ var __lpstr, ___lpstr; __lpstr = ___lpstr = function lpstr_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; var __lpwstr, ___lpwstr; __lpwstr = ___lpwstr = function lpwstr_(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; +var __lpp4, ___lpp4; +__lpp4 = ___lpp4 = function lpp4_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf16le(b, i+4,i+4+len) : "";}; +var __8lpp4, ___8lpp4; +__8lpp4 = ___8lpp4 = function lpp4_8(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len) : "";}; var __double, ___double; __double = ___double = function(b, idx) { return read_double_le(b, idx);}; @@ -1789,6 +1793,8 @@ if(has_buf/*:: && typeof Buffer != 'undefined'*/) { __hexlify = function(b,s,l) { return Buffer.isBuffer(b) ? b.toString('hex',s,s+l) : ___hexlify(b,s,l); }; __lpstr = function lpstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpstr(b, i); var len = b.readUInt32LE(i); return len > 0 ? b.toString('utf8',i+4,i+4+len-1) : "";}; __lpwstr = function lpwstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpwstr(b, i); var len = 2*b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len-1);}; + __lpp4 = function lpp4_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len);}; + __8lpp4 = function lpp4_8b(b,i) { if(!Buffer.isBuffer(b)) return ___8lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf8',i+4,i+4+len);}; __utf8 = function utf8_b(b, s,e) { return b.toString('utf8',s,e); }; __toBuffer = function(bufs) { return (bufs[0].length > 0 && Buffer.isBuffer(bufs[0][0])) ? Buffer.concat(bufs[0]) : ___toBuffer(bufs);}; bconcat = function(bufs) { return Buffer.isBuffer(bufs[0]) ? Buffer.concat(bufs) : [].concat.apply([], bufs); }; @@ -1802,6 +1808,8 @@ if(typeof cptable !== 'undefined') { __utf8 = function(b,s,e) { return cptable.utils.decode(65001, b.slice(s,e)); }; __lpstr = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(current_codepage, b.slice(i+4, i+4+len-1)) : "";}; __lpwstr = function(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len-1)) : "";}; + __lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len)) : "";}; + __8lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(65001, b.slice(i+4,i+4+len)) : "";}; } var __readUInt8 = function(b, idx) { return b[idx]; }; @@ -1835,6 +1843,10 @@ function ReadShift(size/*:number*/, t/*:?string*/) { case 'lpstr': o = __lpstr(this, this.l); size = 5 + o.length; break; /* [MS-OLEDS] 2.1.5 LengthPrefixedUnicodeString */ case 'lpwstr': o = __lpwstr(this, this.l); size = 5 + o.length; if(o[o.length-1] == '\u0000') size += 2; break; + /* [MS-OFFCRYPTO] 2.1.2 Length-Prefixed Padded Unicode String (UNICODE-LP-P4) */ + case 'lpp4': size = 4 + __readUInt32LE(this, this.l); o = __lpp4(this, this.l); if(size & 0x02) size += 2; break; + /* [MS-OFFCRYPTO] 2.1.3 Length-Prefixed UTF-8 String (UTF-8-LP-P4) */ + case '8lpp4': size = 4 + __readUInt32LE(this, this.l); o = __8lpp4(this, this.l); if(size & 0x03) size += 4 - (size & 0x03); break; case 'cstr': size = 0; o = ""; while((w=__readUInt8(this, this.l + size++))!==0) oo.push(_getchar(w)); @@ -4406,6 +4418,16 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.257 */ +function parse_Setup(blob, length, opts) { + var o = {}; + blob.l += 16; + o.header = parse_Xnum(blob, 8); + o.footer = parse_Xnum(blob, 8); + blob.l += 2; + return o; +} + /* 2.4.261 */ function parse_ShtProps(blob, length, opts) { var def = {area:false}; @@ -4415,7 +4437,6 @@ function parse_ShtProps(blob, length, opts) { return def; } - var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -4498,7 +4519,6 @@ var parse_FnGroupName = parsenoop; var parse_FilterMode = parsenoop; var parse_AutoFilterInfo = parsenoop; var parse_AutoFilter = parsenoop; -var parse_Setup = parsenoop; var parse_ScenMan = parsenoop; var parse_SCENARIO = parsenoop; var parse_SxView = parsenoop; @@ -5870,38 +5890,154 @@ function _JS2ANSI(str/*:string*/)/*:Array*/ { } /* [MS-OFFCRYPTO] 2.1.4 Version */ -function parse_Version(blob, length/*:number*/) { +function parse_CRYPTOVersion(blob, length/*:number*/) { var o = {}; o.Major = blob.read_shift(2); o.Minor = blob.read_shift(2); return o; } + +/* [MS-OFFCRYPTO] 2.1.5 DataSpaceVersionInfo */ +function parse_DataSpaceVersionInfo(blob, length) { + var o = {}; + o.id = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6.1 DataSpaceMapEntry Structure */ +function parse_DataSpaceMapEntry(blob) { + var len = blob.read_shift(4); + var end = blob.l + len - 4; + var o = {}; + var cnt = blob.read_shift(4); + var comps = []; + while(cnt-- > 0) { + /* [MS-OFFCRYPTO] 2.1.6.2 DataSpaceReferenceComponent Structure */ + var rc = {}; + rc.t = blob.read_shift(4); + rc.v = blob.read_shift(0, 'lpp4'); + comps.push(rc); + } + o.name = blob.read_shift(0, 'lpp4'); + o.comps = comps; + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6 DataSpaceMap */ +function parse_DataSpaceMap(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(parse_DataSpaceMapEntry(blob)); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.7 DataSpaceDefinition */ +function parse_DataSpaceDefinition(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(blob.read_shift(0, 'lpp4')); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.8 DataSpaceDefinition */ +function parse_TransformInfoHeader(blob, length) { + var o = {}; + var len = blob.read_shift(4); + var tgt = blob.l + len - 4; + blob.l += 4; // must be 0x1 + o.id = blob.read_shift(0, 'lpp4'); + // tgt == len + o.name = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +function parse_Primary(blob, length) { + /* [MS-OFFCRYPTO] 2.2.6 IRMDSTransformInfo */ + var hdr = parse_TransformInfoHeader(blob); + /* [MS-OFFCRYPTO] 2.1.9 EncryptionTransformInfo */ + hdr.ename = blob.read_shift(0, '8lpp4'); + hdr.blksz = blob.read_shift(4); + hdr.cmode = blob.read_shift(4); + if(blob.read_shift(4) != 0x04) throw new Error("Bad !Primary record"); + return hdr; +} + /* [MS-OFFCRYPTO] 2.3.2 Encryption Header */ function parse_EncryptionHeader(blob, length/*:number*/) { + var tgt = blob.l + length; var o = {}; - o.Flags = blob.read_shift(4); - - // Check if SizeExtra is 0x00000000 - var tmp = blob.read_shift(4); - if(tmp !== 0) throw 'Unrecognized SizeExtra: ' + tmp; - + o.Flags = (blob.read_shift(4) & 0x3F); + blob.l += 4; o.AlgID = blob.read_shift(4); + var valid = false; switch(o.AlgID) { - case 0: case 0x6801: case 0x660E: case 0x660F: case 0x6610: break; + case 0x660E: case 0x660F: case 0x6610: valid = (o.Flags == 0x24); break; + case 0x6801: valid = (o.Flags == 0x04); break; + case 0: valid = (o.Flags == 0x10 || o.Flags == 0x04 || o.Flags == 0x24); break; default: throw 'Unrecognized encryption algorithm: ' + o.AlgID; } - parsenoop(blob, length-12); + if(!valid) throw new Error("Encryption Flags/AlgID mismatch"); + o.AlgIDHash = blob.read_shift(4); + o.KeySize = blob.read_shift(4); + o.ProviderType = blob.read_shift(4); + blob.l += 8; + o.CSPName = blob.read_shift((tgt-blob.l)>>1, 'utf16le').slice(0,-1); + blob.l = tgt; return o; } /* [MS-OFFCRYPTO] 2.3.3 Encryption Verifier */ function parse_EncryptionVerifier(blob, length/*:number*/) { - return parsenoop(blob, length); + var o = {}; + blob.l += 4; // SaltSize must be 0x10 + o.Salt = blob.slice(blob.l, blob.l+16); blob.l += 16; + o.Verifier = blob.slice(blob.l, blob.l+16); blob.l += 16; + var sz = blob.read_shift(4); + o.VerifierHash = blob.slice(blob.l, blob.l + sz); blob.l += sz; + return o; } + +/* [MS-OFFCRYPTO] 2.3.4.* EncryptionInfo Stream */ +function parse_EncryptionInfo(blob, length) { + var vers = parse_CRYPTOVersion(blob); + switch(vers.Minor) { + case 0x02: return parse_EncInfoStd(blob, vers); + case 0x03: return parse_EncInfoExt(blob, vers); + case 0x04: return parse_EncInfoAgl(blob, vers); + } + throw new Error("ECMA-376 Encryped file unrecognized Version: " + vers.Minor); +} + +/* [MS-OFFCRYPTO] 2.3.4.5 EncryptionInfo Stream (Standard Encryption) */ +function parse_EncInfoStd(blob, vers) { + var flags = blob.read_shift(4); + if((flags & 0x3F) != 0x24) throw new Error("EncryptionInfo mismatch"); + var sz = blob.read_shift(4); + var tgt = blob.l + sz; + var hdr = parse_EncryptionHeader(blob, sz); + var verifier = parse_EncryptionVerifier(blob, blob.length - blob.l); + return { t:"Std", h:hdr, v:verifier }; +} +/* [MS-OFFCRYPTO] 2.3.4.6 EncryptionInfo Stream (Extensible Encryption) */ +function parse_EncInfoExt(blob, vers) { throw new Error("File is password-protected: ECMA-376 Extensible"); } +/* [MS-OFFCRYPTO] 2.3.4.10 EncryptionInfo Stream (Agile Encryption) */ +function parse_EncInfoAgl(blob, vers) { throw new Error("File is password-protected: ECMA-376 Agile"); } + + + + /* [MS-OFFCRYPTO] 2.3.5.1 RC4 CryptoAPI Encryption Header */ function parse_RC4CryptoHeader(blob, length/*:number*/) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Minor != 2) throw 'unrecognized minor version code: ' + vers.Minor; if(vers.Major > 4 || vers.Major < 2) throw 'unrecognized major version code: ' + vers.Major; o.Flags = blob.read_shift(4); length -= 4; @@ -5913,7 +6049,7 @@ function parse_RC4CryptoHeader(blob, length/*:number*/) { /* [MS-OFFCRYPTO] 2.3.6.1 RC4 Encryption Header */ function parse_RC4Header(blob, length/*:number*/) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Major != 1 || vers.Minor != 1) throw 'unrecognized version code ' + vers.Major + ' : ' + vers.Minor; o.Salt = blob.read_shift(16); o.EncryptedVerifier = blob.read_shift(16); @@ -9674,6 +9810,18 @@ function col_obj_w(C/*:number*/, col) { return p; } +function default_margins(margins, mode) { + if(!margins) return; + var defs = [0.7, 0.7, 0.75, 0.75, 0.3, 0.3]; + if(mode == 'xlml') defs = [1, 1, 1, 1, 0.5, 0.5]; + if(margins.left == null) margins.left = defs[0]; + if(margins.right == null) margins.right = defs[1]; + if(margins.top == null) margins.top = defs[2]; + if(margins.bottom == null) margins.bottom = defs[3]; + if(margins.header == null) margins.header = defs[4]; + if(margins.footer == null) margins.footer = defs[5]; +} + function get_cell_style(styles, cell, opts) { var z = opts.revssf[cell.z != null ? cell.z : "General"]; for(var i = 0, len = styles.length; i != len; ++i) if(styles[i].numFmtId === z) return i; @@ -9732,6 +9880,7 @@ var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; +var marginregex= /<(?:\w:)?pageMargins[^>]*\/>/g; /* 18.3 Worksheets */ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; @@ -9781,6 +9930,10 @@ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksh var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); + /* 18.3.1.62 pageMargins CT_PageMargins */ + var margins = data2.match(marginregex); + if(margins) s['!margins'] = parse_ws_xml_margins(parsexmltag(margins[0])); + if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); if(opts.sheetRows > 0 && s["!ref"]) { var tmpref = safe_decode_range(s["!ref"]); @@ -9855,6 +10008,14 @@ function parse_ws_xml_hlinks(s, data/*:Array*/, rels) { } } +function parse_ws_xml_margins(margin) { + var o = {}; + ["left", "right", "top", "bottom", "header", "footer"].forEach(function(k) { + if(margin[k]) o[k] = parseFloat(margin[k]); + }); + return o; +} + function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { @@ -10491,6 +10652,29 @@ function write_BrtColInfo(C/*:number*/, col, o) { return o; } +/* [MS-XLSB] 2.4.672 BrtMargins */ +function parse_BrtMargins(data, length, opts) { + return { + left: parse_Xnum(data, 8), + right: parse_Xnum(data, 8), + top: parse_Xnum(data, 8), + bottom: parse_Xnum(data, 8), + header: parse_Xnum(data, 8), + footer: parse_Xnum(data, 8) + }; +} +function write_BrtMargins(margins, o) { + if(o == null) o = new_buf(6*8); + default_margins(margins); + write_Xnum(margins.left, o); + write_Xnum(margins.right, o); + write_Xnum(margins.top, o); + write_Xnum(margins.bottom, o); + write_Xnum(margins.header, o); + write_Xnum(margins.footer, o); + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -10669,6 +10853,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { s['!autofilter'] = { ref:encode_range(val) }; break; + case 0x01DC: /* 'BrtMargins' */ + s['!margins'] = val; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -10700,7 +10888,6 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { case 0x0227: /* 'BrtLegacyDrawing' */ case 0x0228: /* 'BrtLegacyDrawingHF' */ case 0x0295: /* 'BrtListPart' */ - case 0x01DC: /* 'BrtMargins' */ case 0x027F: /* 'BrtOleObject' */ case 0x01DE: /* 'BrtPageSetup' */ case 0x0097: /* 'BrtPane' */ @@ -10906,7 +11093,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { /* [DVALS] */ write_HLINKS(ba, ws, rels); /* [BrtPrintOptions] */ - /* [BrtMargins] */ + if(ws['!margins']) write_record(ba, "BrtMargins", write_BrtMargins(ws['!margins'])); /* [BrtPageSetup] */ /* [HEADERFOOTER] */ /* [RWBRK] */ @@ -12245,6 +12432,22 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } else pidx = Rn.index + Rn[0].length; break; + case 'Header': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].header = parsexmltag(Rn[0]).Margin; + break; + case 'Footer': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].footer = parsexmltag(Rn[0]).Margin; + break; + case 'PageMargins': + var pagemargins = parsexmltag(Rn[0]); + if(!cursheet['!margins']) default_margins(cursheet['!margins']={},'xlml'); + if(pagemargins.Top) cursheet['!margins'].top = pagemargins.Top; + if(pagemargins.Left) cursheet['!margins'].left = pagemargins.Left; + if(pagemargins.Right) cursheet['!margins'].right = pagemargins.Right; + if(pagemargins.Bottom) cursheet['!margins'].bottom = pagemargins.Bottom; + break; case 'Unsynced': break; case 'Print': break; case 'Panes': break; @@ -12252,10 +12455,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Pane': break; case 'Number': break; case 'Layout': break; - case 'Header': break; - case 'Footer': break; case 'PageSetup': break; - case 'PageMargins': break; case 'Selected': break; case 'ProtectObjects': break; case 'EnableSelection': break; @@ -13175,12 +13375,30 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': break; // TODO + case 'LeftMargin': + case 'RightMargin': + case 'TopMargin': + case 'BottomMargin': + if(!out['!margins']) default_margins(out['!margins'] = {}); + switch(Rn) { + case 'LeftMargin': out['!margins'].left = val; break; + case 'RightMargin': out['!margins'].right = val; break; + case 'TopMargin': out['!margins'].top = val; break; + case 'BottomMargin': out['!margins'].bottom = val; break; + } + break; + + case 'Setup': // TODO + if(!out['!margins']) default_margins(out['!margins'] = {}); + out['!margins'].header = val.header; + out['!margins'].footer = val.footer; + break; + case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO case 'VCenter': break; // TODO case 'Pls': break; // TODO - case 'Setup': break; // TODO case 'GCW': break; case 'LHRecord': break; case 'DBCell': break; // TODO @@ -13375,7 +13593,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'WebPub': case 'AutoWebPub': /* Print Stuff */ - case 'RightMargin': case 'LeftMargin': case 'TopMargin': case 'BottomMargin': case 'HeaderFooter': case 'HFPicture': case 'PLV': case 'HorizontalPageBreaks': case 'VerticalPageBreaks': /* Behavioral */ @@ -13428,7 +13645,6 @@ fix_read_opts(options); reset_cp(); var CompObj, Summary, Workbook/*:?any*/; if(cfb.FullPaths) { - if(cfb.find("EncryptedPackage")) throw new Error("File is password-protected"); CompObj = cfb.find('!CompObj'); Summary = cfb.find('!SummaryInformation'); Workbook = cfb.find('/Workbook'); @@ -13881,7 +14097,7 @@ var XLSBRecordEnum = { /*::[*/0x01D9/*::]*/: { n:"BrtBeginColorPalette", f:parsenoop }, /*::[*/0x01DA/*::]*/: { n:"BrtEndColorPalette", f:parsenoop }, /*::[*/0x01DB/*::]*/: { n:"BrtIndexedColor", f:parsenoop }, - /*::[*/0x01DC/*::]*/: { n:"BrtMargins", f:parsenoop }, + /*::[*/0x01DC/*::]*/: { n:"BrtMargins", f:parse_BrtMargins }, /*::[*/0x01DD/*::]*/: { n:"BrtPrintOptions", f:parsenoop }, /*::[*/0x01DE/*::]*/: { n:"BrtPageSetup", f:parsenoop }, /*::[*/0x01DF/*::]*/: { n:"BrtBeginHeaderFooter", f:parsenoop }, @@ -15692,6 +15908,44 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { } return out; } + +/* references to [MS-OFFCRYPTO] */ +function parse_xlsxcfb(cfb, opts/*:?ParseOpts*/)/*:Workbook*/ { + var f = 'Version'; + var data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var version = parse_DataSpaceVersionInfo(data.content); + + /* 2.3.4.1 */ + f = 'DataSpaceMap'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var dsm = parse_DataSpaceMap(data.content); + if(dsm.length != 1 || dsm[0].comps.length != 1 || dsm[0].comps[0].t != 0 || + dsm[0].name != "StrongEncryptionDataSpace" || dsm[0].comps[0].v != "EncryptedPackage") + throw new Error("ECMA-376 Encrypted file bad " + f); + + f = 'StrongEncryptionDataSpace'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var seds = parse_DataSpaceDefinition(data.content); + if(seds.length != 1 || seds[0] != "StrongEncryptionTransform") + throw new Error("ECMA-376 Encrypted file bad " + f); + + /* 2.3.4.3 */ + f = '!Primary'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var hdr = parse_Primary(data.content); + + f = 'EncryptionInfo'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var einfo = parse_EncryptionInfo(data.content); + + throw new Error("File is password-protected"); +} + function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { _shapeid = 1024; if(opts.bookType == "ods") return write_ods(wb, opts); @@ -15834,6 +16088,11 @@ function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:Array*/ { return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } +function read_cfb(cfb, opts/*:?ParseOpts*/)/*:Workbook*/ { + if(cfb.find("EncryptedPackage")) return parse_xlsxcfb(cfb, opts); + return parse_xlscfb(cfb, opts); +} + function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { /*:: if(!jszip) throw new Error("JSZip is not available"); */ var zip, d = data; @@ -15863,7 +16122,7 @@ function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } switch((n = firstbyte(d, o))[0]) { - case 0xD0: return parse_xlscfb(CFB.read(d, o), o); + case 0xD0: return read_cfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); case 0x49: if(n[1] == 0x44) return SYLK.to_workbook(d, o); break; diff --git a/xlsx.js b/xlsx.js index c7916f8..8519e87 100644 --- a/xlsx.js +++ b/xlsx.js @@ -1729,6 +1729,10 @@ var __lpstr, ___lpstr; __lpstr = ___lpstr = function lpstr_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; var __lpwstr, ___lpwstr; __lpwstr = ___lpwstr = function lpwstr_(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len-1) : "";}; +var __lpp4, ___lpp4; +__lpp4 = ___lpp4 = function lpp4_(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf16le(b, i+4,i+4+len) : "";}; +var __8lpp4, ___8lpp4; +__8lpp4 = ___8lpp4 = function lpp4_8(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? __utf8(b, i+4,i+4+len) : "";}; var __double, ___double; __double = ___double = function(b, idx) { return read_double_le(b, idx);}; @@ -1738,6 +1742,8 @@ if(has_buf) { __hexlify = function(b,s,l) { return Buffer.isBuffer(b) ? b.toString('hex',s,s+l) : ___hexlify(b,s,l); }; __lpstr = function lpstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpstr(b, i); var len = b.readUInt32LE(i); return len > 0 ? b.toString('utf8',i+4,i+4+len-1) : "";}; __lpwstr = function lpwstr_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpwstr(b, i); var len = 2*b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len-1);}; + __lpp4 = function lpp4_b(b,i) { if(!Buffer.isBuffer(b)) return ___lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf16le',i+4,i+4+len);}; + __8lpp4 = function lpp4_8b(b,i) { if(!Buffer.isBuffer(b)) return ___8lpp4(b, i); var len = b.readUInt32LE(i); return b.toString('utf8',i+4,i+4+len);}; __utf8 = function utf8_b(b, s,e) { return b.toString('utf8',s,e); }; __toBuffer = function(bufs) { return (bufs[0].length > 0 && Buffer.isBuffer(bufs[0][0])) ? Buffer.concat(bufs[0]) : ___toBuffer(bufs);}; bconcat = function(bufs) { return Buffer.isBuffer(bufs[0]) ? Buffer.concat(bufs) : [].concat.apply([], bufs); }; @@ -1751,6 +1757,8 @@ if(typeof cptable !== 'undefined') { __utf8 = function(b,s,e) { return cptable.utils.decode(65001, b.slice(s,e)); }; __lpstr = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(current_codepage, b.slice(i+4, i+4+len-1)) : "";}; __lpwstr = function(b,i) { var len = 2*__readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len-1)) : "";}; + __lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(1200, b.slice(i+4,i+4+len)) : "";}; + __8lpp4 = function(b,i) { var len = __readUInt32LE(b,i); return len > 0 ? cptable.utils.decode(65001, b.slice(i+4,i+4+len)) : "";}; } var __readUInt8 = function(b, idx) { return b[idx]; }; @@ -1784,6 +1792,10 @@ function ReadShift(size, t) { case 'lpstr': o = __lpstr(this, this.l); size = 5 + o.length; break; /* [MS-OLEDS] 2.1.5 LengthPrefixedUnicodeString */ case 'lpwstr': o = __lpwstr(this, this.l); size = 5 + o.length; if(o[o.length-1] == '\u0000') size += 2; break; + /* [MS-OFFCRYPTO] 2.1.2 Length-Prefixed Padded Unicode String (UNICODE-LP-P4) */ + case 'lpp4': size = 4 + __readUInt32LE(this, this.l); o = __lpp4(this, this.l); if(size & 0x02) size += 2; break; + /* [MS-OFFCRYPTO] 2.1.3 Length-Prefixed UTF-8 String (UTF-8-LP-P4) */ + case '8lpp4': size = 4 + __readUInt32LE(this, this.l); o = __8lpp4(this, this.l); if(size & 0x03) size += 4 - (size & 0x03); break; case 'cstr': size = 0; o = ""; while((w=__readUInt8(this, this.l + size++))!==0) oo.push(_getchar(w)); @@ -4350,6 +4362,16 @@ function parse_ColInfo(blob, length, opts) { return {s:colFirst, e:colLast, w:coldx, ixfe:ixfe, flags:flags}; } +/* 2.4.257 */ +function parse_Setup(blob, length, opts) { + var o = {}; + blob.l += 16; + o.header = parse_Xnum(blob, 8); + o.footer = parse_Xnum(blob, 8); + blob.l += 2; + return o; +} + /* 2.4.261 */ function parse_ShtProps(blob, length, opts) { var def = {area:false}; @@ -4359,7 +4381,6 @@ function parse_ShtProps(blob, length, opts) { return def; } - var parse_Style = parsenoop; var parse_StyleExt = parsenoop; @@ -4442,7 +4463,6 @@ var parse_FnGroupName = parsenoop; var parse_FilterMode = parsenoop; var parse_AutoFilterInfo = parsenoop; var parse_AutoFilter = parsenoop; -var parse_Setup = parsenoop; var parse_ScenMan = parsenoop; var parse_SCENARIO = parsenoop; var parse_SxView = parsenoop; @@ -5814,38 +5834,154 @@ function _JS2ANSI(str) { } /* [MS-OFFCRYPTO] 2.1.4 Version */ -function parse_Version(blob, length) { +function parse_CRYPTOVersion(blob, length) { var o = {}; o.Major = blob.read_shift(2); o.Minor = blob.read_shift(2); return o; } + +/* [MS-OFFCRYPTO] 2.1.5 DataSpaceVersionInfo */ +function parse_DataSpaceVersionInfo(blob, length) { + var o = {}; + o.id = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6.1 DataSpaceMapEntry Structure */ +function parse_DataSpaceMapEntry(blob) { + var len = blob.read_shift(4); + var end = blob.l + len - 4; + var o = {}; + var cnt = blob.read_shift(4); + var comps = []; + while(cnt-- > 0) { + /* [MS-OFFCRYPTO] 2.1.6.2 DataSpaceReferenceComponent Structure */ + var rc = {}; + rc.t = blob.read_shift(4); + rc.v = blob.read_shift(0, 'lpp4'); + comps.push(rc); + } + o.name = blob.read_shift(0, 'lpp4'); + o.comps = comps; + return o; +} + +/* [MS-OFFCRYPTO] 2.1.6 DataSpaceMap */ +function parse_DataSpaceMap(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(parse_DataSpaceMapEntry(blob)); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.7 DataSpaceDefinition */ +function parse_DataSpaceDefinition(blob, length) { + var o = []; + blob.l += 4; // must be 0x8 + var cnt = blob.read_shift(4); + while(cnt-- > 0) o.push(blob.read_shift(0, 'lpp4')); + return o; +} + +/* [MS-OFFCRYPTO] 2.1.8 DataSpaceDefinition */ +function parse_TransformInfoHeader(blob, length) { + var o = {}; + var len = blob.read_shift(4); + var tgt = blob.l + len - 4; + blob.l += 4; // must be 0x1 + o.id = blob.read_shift(0, 'lpp4'); + // tgt == len + o.name = blob.read_shift(0, 'lpp4'); + o.R = parse_CRYPTOVersion(blob, 4); + o.U = parse_CRYPTOVersion(blob, 4); + o.W = parse_CRYPTOVersion(blob, 4); + return o; +} + +function parse_Primary(blob, length) { + /* [MS-OFFCRYPTO] 2.2.6 IRMDSTransformInfo */ + var hdr = parse_TransformInfoHeader(blob); + /* [MS-OFFCRYPTO] 2.1.9 EncryptionTransformInfo */ + hdr.ename = blob.read_shift(0, '8lpp4'); + hdr.blksz = blob.read_shift(4); + hdr.cmode = blob.read_shift(4); + if(blob.read_shift(4) != 0x04) throw new Error("Bad !Primary record"); + return hdr; +} + /* [MS-OFFCRYPTO] 2.3.2 Encryption Header */ function parse_EncryptionHeader(blob, length) { + var tgt = blob.l + length; var o = {}; - o.Flags = blob.read_shift(4); - - // Check if SizeExtra is 0x00000000 - var tmp = blob.read_shift(4); - if(tmp !== 0) throw 'Unrecognized SizeExtra: ' + tmp; - + o.Flags = (blob.read_shift(4) & 0x3F); + blob.l += 4; o.AlgID = blob.read_shift(4); + var valid = false; switch(o.AlgID) { - case 0: case 0x6801: case 0x660E: case 0x660F: case 0x6610: break; + case 0x660E: case 0x660F: case 0x6610: valid = (o.Flags == 0x24); break; + case 0x6801: valid = (o.Flags == 0x04); break; + case 0: valid = (o.Flags == 0x10 || o.Flags == 0x04 || o.Flags == 0x24); break; default: throw 'Unrecognized encryption algorithm: ' + o.AlgID; } - parsenoop(blob, length-12); + if(!valid) throw new Error("Encryption Flags/AlgID mismatch"); + o.AlgIDHash = blob.read_shift(4); + o.KeySize = blob.read_shift(4); + o.ProviderType = blob.read_shift(4); + blob.l += 8; + o.CSPName = blob.read_shift((tgt-blob.l)>>1, 'utf16le').slice(0,-1); + blob.l = tgt; return o; } /* [MS-OFFCRYPTO] 2.3.3 Encryption Verifier */ function parse_EncryptionVerifier(blob, length) { - return parsenoop(blob, length); + var o = {}; + blob.l += 4; // SaltSize must be 0x10 + o.Salt = blob.slice(blob.l, blob.l+16); blob.l += 16; + o.Verifier = blob.slice(blob.l, blob.l+16); blob.l += 16; + var sz = blob.read_shift(4); + o.VerifierHash = blob.slice(blob.l, blob.l + sz); blob.l += sz; + return o; } + +/* [MS-OFFCRYPTO] 2.3.4.* EncryptionInfo Stream */ +function parse_EncryptionInfo(blob, length) { + var vers = parse_CRYPTOVersion(blob); + switch(vers.Minor) { + case 0x02: return parse_EncInfoStd(blob, vers); + case 0x03: return parse_EncInfoExt(blob, vers); + case 0x04: return parse_EncInfoAgl(blob, vers); + } + throw new Error("ECMA-376 Encryped file unrecognized Version: " + vers.Minor); +} + +/* [MS-OFFCRYPTO] 2.3.4.5 EncryptionInfo Stream (Standard Encryption) */ +function parse_EncInfoStd(blob, vers) { + var flags = blob.read_shift(4); + if((flags & 0x3F) != 0x24) throw new Error("EncryptionInfo mismatch"); + var sz = blob.read_shift(4); + var tgt = blob.l + sz; + var hdr = parse_EncryptionHeader(blob, sz); + var verifier = parse_EncryptionVerifier(blob, blob.length - blob.l); + return { t:"Std", h:hdr, v:verifier }; +} +/* [MS-OFFCRYPTO] 2.3.4.6 EncryptionInfo Stream (Extensible Encryption) */ +function parse_EncInfoExt(blob, vers) { throw new Error("File is password-protected: ECMA-376 Extensible"); } +/* [MS-OFFCRYPTO] 2.3.4.10 EncryptionInfo Stream (Agile Encryption) */ +function parse_EncInfoAgl(blob, vers) { throw new Error("File is password-protected: ECMA-376 Agile"); } + + + + /* [MS-OFFCRYPTO] 2.3.5.1 RC4 CryptoAPI Encryption Header */ function parse_RC4CryptoHeader(blob, length) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Minor != 2) throw 'unrecognized minor version code: ' + vers.Minor; if(vers.Major > 4 || vers.Major < 2) throw 'unrecognized major version code: ' + vers.Major; o.Flags = blob.read_shift(4); length -= 4; @@ -5857,7 +5993,7 @@ function parse_RC4CryptoHeader(blob, length) { /* [MS-OFFCRYPTO] 2.3.6.1 RC4 Encryption Header */ function parse_RC4Header(blob, length) { var o = {}; - var vers = o.EncryptionVersionInfo = parse_Version(blob, 4); length -= 4; + var vers = o.EncryptionVersionInfo = parse_CRYPTOVersion(blob, 4); length -= 4; if(vers.Major != 1 || vers.Minor != 1) throw 'unrecognized version code ' + vers.Major + ' : ' + vers.Minor; o.Salt = blob.read_shift(16); o.EncryptedVerifier = blob.read_shift(16); @@ -9617,6 +9753,18 @@ function col_obj_w(C, col) { return p; } +function default_margins(margins, mode) { + if(!margins) return; + var defs = [0.7, 0.7, 0.75, 0.75, 0.3, 0.3]; + if(mode == 'xlml') defs = [1, 1, 1, 1, 0.5, 0.5]; + if(margins.left == null) margins.left = defs[0]; + if(margins.right == null) margins.right = defs[1]; + if(margins.top == null) margins.top = defs[2]; + if(margins.bottom == null) margins.bottom = defs[3]; + if(margins.header == null) margins.header = defs[4]; + if(margins.footer == null) margins.footer = defs[5]; +} + function get_cell_style(styles, cell, opts) { var z = opts.revssf[cell.z != null ? cell.z : "General"]; for(var i = 0, len = styles.length; i != len; ++i) if(styles[i].numFmtId === z) return i; @@ -9675,6 +9823,7 @@ var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; +var marginregex= /<(?:\w:)?pageMargins[^>]*\/>/g; /* 18.3 Worksheets */ function parse_ws_xml(data, opts, rels, wb, themes, styles) { if(!data) return data; @@ -9724,6 +9873,10 @@ function parse_ws_xml(data, opts, rels, wb, themes, styles) { var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); + /* 18.3.1.62 pageMargins CT_PageMargins */ + var margins = data2.match(marginregex); + if(margins) s['!margins'] = parse_ws_xml_margins(parsexmltag(margins[0])); + if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); if(opts.sheetRows > 0 && s["!ref"]) { var tmpref = safe_decode_range(s["!ref"]); @@ -9798,6 +9951,14 @@ function parse_ws_xml_hlinks(s, data, rels) { } } +function parse_ws_xml_margins(margin) { + var o = {}; + ["left", "right", "top", "bottom", "header", "footer"].forEach(function(k) { + if(margin[k]) o[k] = parseFloat(margin[k]); + }); + return o; +} + function parse_ws_xml_cols(columns, cols) { var seencol = false; for(var coli = 0; coli != cols.length; ++coli) { @@ -10434,6 +10595,29 @@ function write_BrtColInfo(C, col, o) { return o; } +/* [MS-XLSB] 2.4.672 BrtMargins */ +function parse_BrtMargins(data, length, opts) { + return { + left: parse_Xnum(data, 8), + right: parse_Xnum(data, 8), + top: parse_Xnum(data, 8), + bottom: parse_Xnum(data, 8), + header: parse_Xnum(data, 8), + footer: parse_Xnum(data, 8) + }; +} +function write_BrtMargins(margins, o) { + if(o == null) o = new_buf(6*8); + default_margins(margins); + write_Xnum(margins.left, o); + write_Xnum(margins.right, o); + write_Xnum(margins.top, o); + write_Xnum(margins.bottom, o); + write_Xnum(margins.header, o); + write_Xnum(margins.footer, o); + return o; +} + /* [MS-XLSB] 2.4.740 BrtSheetProtection */ function write_BrtSheetProtection(sp, o) { if(o == null) o = new_buf(16*4+2); @@ -10612,6 +10796,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { s['!autofilter'] = { ref:encode_range(val) }; break; + case 0x01DC: /* 'BrtMargins' */ + s['!margins'] = val; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -10643,7 +10831,6 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { case 0x0227: /* 'BrtLegacyDrawing' */ case 0x0228: /* 'BrtLegacyDrawingHF' */ case 0x0295: /* 'BrtListPart' */ - case 0x01DC: /* 'BrtMargins' */ case 0x027F: /* 'BrtOleObject' */ case 0x01DE: /* 'BrtPageSetup' */ case 0x0097: /* 'BrtPane' */ @@ -10849,7 +11036,7 @@ function write_ws_bin(idx, opts, wb, rels) { /* [DVALS] */ write_HLINKS(ba, ws, rels); /* [BrtPrintOptions] */ - /* [BrtMargins] */ + if(ws['!margins']) write_record(ba, "BrtMargins", write_BrtMargins(ws['!margins'])); /* [BrtPageSetup] */ /* [HEADERFOOTER] */ /* [RWBRK] */ @@ -12185,6 +12372,22 @@ for(var cma = c; cma <= cc; ++cma) { } else pidx = Rn.index + Rn[0].length; break; + case 'Header': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].header = parsexmltag(Rn[0]).Margin; + break; + case 'Footer': + if(!cursheet['!margins']) default_margins(cursheet['!margins']={}, 'xlml'); + cursheet['!margins'].footer = parsexmltag(Rn[0]).Margin; + break; + case 'PageMargins': + var pagemargins = parsexmltag(Rn[0]); + if(!cursheet['!margins']) default_margins(cursheet['!margins']={},'xlml'); + if(pagemargins.Top) cursheet['!margins'].top = pagemargins.Top; + if(pagemargins.Left) cursheet['!margins'].left = pagemargins.Left; + if(pagemargins.Right) cursheet['!margins'].right = pagemargins.Right; + if(pagemargins.Bottom) cursheet['!margins'].bottom = pagemargins.Bottom; + break; case 'Unsynced': break; case 'Print': break; case 'Panes': break; @@ -12192,10 +12395,7 @@ for(var cma = c; cma <= cc; ++cma) { case 'Pane': break; case 'Number': break; case 'Layout': break; - case 'Header': break; - case 'Footer': break; case 'PageSetup': break; - case 'PageMargins': break; case 'Selected': break; case 'ProtectObjects': break; case 'EnableSelection': break; @@ -13114,12 +13314,30 @@ function parse_workbook(blob, options) { } break; case 'Row': break; // TODO + case 'LeftMargin': + case 'RightMargin': + case 'TopMargin': + case 'BottomMargin': + if(!out['!margins']) default_margins(out['!margins'] = {}); + switch(Rn) { + case 'LeftMargin': out['!margins'].left = val; break; + case 'RightMargin': out['!margins'].right = val; break; + case 'TopMargin': out['!margins'].top = val; break; + case 'BottomMargin': out['!margins'].bottom = val; break; + } + break; + + case 'Setup': // TODO + if(!out['!margins']) default_margins(out['!margins'] = {}); + out['!margins'].header = val.header; + out['!margins'].footer = val.footer; + break; + case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO case 'VCenter': break; // TODO case 'Pls': break; // TODO - case 'Setup': break; // TODO case 'GCW': break; case 'LHRecord': break; case 'DBCell': break; // TODO @@ -13314,7 +13532,6 @@ function parse_workbook(blob, options) { case 'WebPub': case 'AutoWebPub': /* Print Stuff */ - case 'RightMargin': case 'LeftMargin': case 'TopMargin': case 'BottomMargin': case 'HeaderFooter': case 'HFPicture': case 'PLV': case 'HorizontalPageBreaks': case 'VerticalPageBreaks': /* Behavioral */ @@ -13367,7 +13584,6 @@ fix_read_opts(options); reset_cp(); var CompObj, Summary, Workbook; if(cfb.FullPaths) { - if(cfb.find("EncryptedPackage")) throw new Error("File is password-protected"); CompObj = cfb.find('!CompObj'); Summary = cfb.find('!SummaryInformation'); Workbook = cfb.find('/Workbook'); @@ -13820,7 +14036,7 @@ var XLSBRecordEnum = { 0x01D9: { n:"BrtBeginColorPalette", f:parsenoop }, 0x01DA: { n:"BrtEndColorPalette", f:parsenoop }, 0x01DB: { n:"BrtIndexedColor", f:parsenoop }, -0x01DC: { n:"BrtMargins", f:parsenoop }, +0x01DC: { n:"BrtMargins", f:parse_BrtMargins }, 0x01DD: { n:"BrtPrintOptions", f:parsenoop }, 0x01DE: { n:"BrtPageSetup", f:parsenoop }, 0x01DF: { n:"BrtBeginHeaderFooter", f:parsenoop }, @@ -15630,6 +15846,44 @@ function parse_zip(zip, opts) { } return out; } + +/* references to [MS-OFFCRYPTO] */ +function parse_xlsxcfb(cfb, opts) { + var f = 'Version'; + var data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var version = parse_DataSpaceVersionInfo(data.content); + + /* 2.3.4.1 */ + f = 'DataSpaceMap'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var dsm = parse_DataSpaceMap(data.content); + if(dsm.length != 1 || dsm[0].comps.length != 1 || dsm[0].comps[0].t != 0 || + dsm[0].name != "StrongEncryptionDataSpace" || dsm[0].comps[0].v != "EncryptedPackage") + throw new Error("ECMA-376 Encrypted file bad " + f); + + f = 'StrongEncryptionDataSpace'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var seds = parse_DataSpaceDefinition(data.content); + if(seds.length != 1 || seds[0] != "StrongEncryptionTransform") + throw new Error("ECMA-376 Encrypted file bad " + f); + + /* 2.3.4.3 */ + f = '!Primary'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var hdr = parse_Primary(data.content); + + f = 'EncryptionInfo'; + data = cfb.find(f); + if(!data) throw new Error("ECMA-376 Encrypted file missing " + f); + var einfo = parse_EncryptionInfo(data.content); + + throw new Error("File is password-protected"); +} + function write_zip(wb, opts) { _shapeid = 1024; if(opts.bookType == "ods") return write_ods(wb, opts); @@ -15770,6 +16024,11 @@ function firstbyte(f,o) { return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } +function read_cfb(cfb, opts) { + if(cfb.find("EncryptedPackage")) return parse_xlsxcfb(cfb, opts); + return parse_xlscfb(cfb, opts); +} + function read_zip(data, opts) { var zip, d = data; var o = opts||{}; @@ -15798,7 +16057,7 @@ function readSync(data, opts) { if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } switch((n = firstbyte(d, o))[0]) { - case 0xD0: return parse_xlscfb(CFB.read(d, o), o); + case 0xD0: return read_cfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); case 0x49: if(n[1] == 0x44) return SYLK.to_workbook(d, o); break;