From d3eaa62d4577d16e78ff32f23df0374d653b199c Mon Sep 17 00:00:00 2001
From: SheetJS <dev@sheetjs.com>
Date: Wed, 15 Mar 2017 04:19:02 -0400
Subject: [PATCH] unify stub cells with type `z`

- fixes #382 h/t @jugaltheshah @pimpelsang
- fixes #333 h/t @xushuheng0623 @abhishek1234321 @mateuszkrzeszowiec
- fixes #79 h/t @volodymyrl @elad
---
 CHANGELOG.md        |  1 +
 README.md           |  6 ++++-
 bits/39_xlsbiff.js  | 12 ++++++++-
 bits/66_wscommon.js |  1 +
 bits/67_wsxml.js    |  8 +++---
 bits/68_wsbin.js    |  2 +-
 bits/75_xlml.js     | 15 +++++++++--
 bits/76_xls.js      | 16 +++++++++--
 bits/80_parseods.js |  8 +++---
 bits/90_utils.js    |  4 ++-
 test.js             | 14 +++++++---
 xlsx.flow.js        | 66 +++++++++++++++++++++++++++++++++++----------
 xlsx.js             | 65 ++++++++++++++++++++++++++++++++++----------
 13 files changed, 172 insertions(+), 46 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eb66a1..614af48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ changes may not be included if they are not expected to break existing code.
 ## Unreleased
 
 * XLML property names are more closely mapped to the XLSX equivalent
+* Stub cells are now cell type `z`
 
 ## 0.9.2 (2017-03-13)
 
diff --git a/README.md b/README.md
index bbd9a68..b1f30d2 100644
--- a/README.md
+++ b/README.md
@@ -477,6 +477,10 @@ dates in the local timezone.  js-xlsx does not correct for this error.
 Type `s` is the String type.  `v` should be explicitly stored as a string to
 avoid possible confusion.
 
+Type `z` represents blank stub cells.  These do not have any data or type, and
+are not processed by any of the core library functions.  By default these cells
+will not be generated; the parser `cellStubs` option must be set to `true`.
+
 ### Formulae
 
 The A1-style formula string is stored in the `f` field.  Even though different
@@ -549,7 +553,7 @@ The exported `read` and `readFile` functions accept an options argument:
 | cellNF      | false   | Save number format string to the .z field            |
 | cellStyles  | false   | Save style/theme info to the .s field                |
 | cellDates   | false   | Store dates as type `d` (default is `n`) **          |
-| sheetStubs  | false   | Create cell objects for stub cells                   |
+| sheetStubs  | false   | Create cell objects of type `z` for stub cells       |
 | sheetRows   | 0       | If >0, read the first `sheetRows` rows **            |
 | bookDeps    | false   | If true, parse calculation chains                    |
 | bookFiles   | false   | If true, add raw files to book object **             |
diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js
index 8338a74..2ded65b 100644
--- a/bits/39_xlsbiff.js
+++ b/bits/39_xlsbiff.js
@@ -315,6 +315,17 @@ function parse_MulRk(blob, length) {
 	if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch";
 	return {r:rw, c:col, C:lastcol, rkrec:rkrecs};
 }
+/* 2.4.174 */
+function parse_MulBlank(blob, length) {
+	var target = blob.l + length - 2;
+	var rw = blob.read_shift(2), col = blob.read_shift(2);
+	var ixfes = [];
+	while(blob.l < target) ixfes.push(blob.read_shift(2));
+	if(blob.l !== target) throw "MulBlank read error";
+	var lastcol = blob.read_shift(2);
+	if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch";
+	return {r:rw, c:col, C:lastcol, ixfe:ixfes};
+}
 
 /* 2.5.20 2.5.249 TODO: interpret values here */
 function parse_CellStyleXF(blob, length, style) {
@@ -711,7 +722,6 @@ var parse_SXLI = parsenoop;
 var parse_SXPI = parsenoop;
 var parse_DocRoute = parsenoop;
 var parse_RecipName = parsenoop;
-var parse_MulBlank = parsenoop;
 var parse_SXDI = parsenoop;
 var parse_SXDB = parsenoop;
 var parse_SXFDB = parsenoop;
diff --git a/bits/66_wscommon.js b/bits/66_wscommon.js
index 6b01b2b..fb642c6 100644
--- a/bits/66_wscommon.js
+++ b/bits/66_wscommon.js
@@ -23,6 +23,7 @@ function get_cell_style(styles, cell, opts) {
 }
 
 function safe_format(p, fmtid, fillid, opts) {
+	if(p.t === 'z') return;
 	if(p.t === 'd' && typeof p.v === 'string') p.v = new Date(p.v);
 	try {
 		if(p.t === 'e') p.w = p.w || BErr[p.v];
diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js
index feb3d30..6ef817e 100644
--- a/bits/67_wsxml.js
+++ b/bits/67_wsxml.js
@@ -87,7 +87,7 @@ function parse_ws_xml_hlinks(s, data, rels) {
 		var rng = safe_decode_range(val.ref);
 		for(var R=rng.s.r;R<=rng.e.r;++R) for(var C=rng.s.c;C<=rng.e.c;++C) {
 			var addr = encode_cell({c:C,r:R});
-			if(!s[addr]) s[addr] = {t:"stub",v:undefined};
+			if(!s[addr]) s[addr] = {t:"z",v:undefined};
 			s[addr].l = val;
 		}
 	}
@@ -126,7 +126,7 @@ function write_ws_xml_cols(ws, cols)/*:string*/ {
 }
 
 function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) {
-	if(cell.v === undefined) return "";
+	if(cell.v === undefined || cell.t === 'z') return "";
 	var vv = "";
 	var oldt = cell.t, oldv = cell.v;
 	switch(cell.t) {
@@ -239,7 +239,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 			/* SCHEMA IS ACTUALLY INCORRECT HERE.  IF A CELL HAS NO T, EMIT "" */
 			if(tag.t === undefined && p.v === undefined) {
 				if(!opts.sheetStubs) continue;
-				p.t = "stub";
+				p.t = "z";
 			}
 			else p.t = tag.t || "n";
 			if(guess.s.c > idx) guess.s.c = idx;
@@ -251,7 +251,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 					sstr = strs[parseInt(p.v, 10)];
 					if(typeof p.v == 'undefined') {
 						if(!opts.sheetStubs) continue;
-						p.t = "stub";
+						p.t = 'z';
 					}
 					p.v = sstr.t;
 					p.r = sstr.r;
diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js
index d99d765..8c7dbda 100644
--- a/bits/68_wsbin.js
+++ b/bits/68_wsbin.js
@@ -324,7 +324,7 @@ function parse_ws_bin(data, opts, rels, wb)/*:Worksheet*/ {
 				break;
 
 			case 'BrtCellBlank': if(!opts.sheetStubs) break;
-				p = ({t:'s',v:undefined}/*:any*/);
+				p = ({t:'z',v:undefined}/*:any*/);
 				s[encode_col(C=val[0].c) + rr] = p;
 				if(refguess.s.r > row.r) refguess.s.r = row.r;
 				if(refguess.s.c > C) refguess.s.c = C;
diff --git a/bits/75_xlml.js b/bits/75_xlml.js
index e3139d3..0f04859 100644
--- a/bits/75_xlml.js
+++ b/bits/75_xlml.js
@@ -58,6 +58,7 @@ function xlml_set_custprop(Custprops, Rn, cp, val/*:string*/) {
 }
 
 function safe_format_xlml(cell/*:Cell*/, nf, o) {
+	if(cell.t === 'z') return;
 	try {
 		if(cell.t === 'e') { cell.w = cell.w || BErr[cell.v]; }
 		else if(nf === "General") {
@@ -200,8 +201,17 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
 					var rr = r + (parseInt(cell.MergeDown,10)|0);
 					mergecells.push({s:{c:c,r:r},e:{c:cc,r:rr}});
 				}
-				++c;
-				if(cell.MergeAcross) c += +cell.MergeAcross;
+				if(!opts.sheetStubs) { if(cell.MergeAcross) c = cc + 1; else ++c; }
+				else if(cell.MergeAcross || cell.MergeDown) {
+					/*:: if(!cc) cc = 0; if(!rr) rr = 0; */
+					for(var cma = c; cma <= cc; ++cma) {
+						for(var cmd = r; cmd <= rr; ++cmd) {
+							if(cma > c || cmd > r) cursheet[encode_col(cma) + encode_row(cmd)] = {t:'z'};
+						}
+					}
+					c = cc + 1;
+				}
+				else ++c;
 			} else {
 				cell = xlml_parsexmltagobj(Rn[0]);
 				if(cell.Index) c = +cell.Index - 1;
@@ -756,6 +766,7 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{
 
 	var t = "", p = "";
 	switch(cell.t) {
+		case 'z': return "";
 		case 'n': t = 'Number'; p = String(cell.v); break;
 		case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break;
 		case 'e': t = 'Error'; p = BErr[cell.v]; break;
diff --git a/bits/76_xls.js b/bits/76_xls.js
index adf8518..370c8ca 100644
--- a/bits/76_xls.js
+++ b/bits/76_xls.js
@@ -53,6 +53,7 @@ function slurp(R, blob, length/*:number*/, opts) {
 }
 
 function safe_format_xf(p/*:any*/, opts/*:ParseOpts*/, date1904/*:?boolean*/) {
+	if(p.t === 'z') return;
 	if(p.t === 'e') { p.w = p.w || BErr[p.v]; }
 	if(!p.XF) return;
 	try {
@@ -354,6 +355,19 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 					safe_format_xf(temp_val, options, wb.opts.Date1904);
 					addcell({c:val.c, r:val.r}, temp_val, options);
 					break;
+				case 'Blank': if(options.sheetStubs) {
+					temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], t:'z'};
+					safe_format_xf(temp_val, options, wb.opts.Date1904);
+					addcell({c:val.c, r:val.r}, temp_val, options);
+				} break;
+				case 'MulBlank': if(options.sheetStubs) {
+					for(var _j = val.c; _j <= val.C; ++_j) {
+						var _ixfe = val.ixfe[_j-val.c];
+						temp_val= {ixfe:_ixfe, XF:XFs[_ixfe], t:'z'};
+						safe_format_xf(temp_val, options, wb.opts.Date1904);
+						addcell({c:_j, r:val.r}, temp_val, options);
+					}
+				} break;
 				case 'RString':
 				case 'Label': case 'BIFF2STR':
 					temp_val=make_cell(val.val, val.ixfe, 's');
@@ -420,7 +434,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 				case 'ColInfo': break; // TODO
 				case 'Row': break; // TODO
 				case 'DBCell': break; // TODO
-				case 'MulBlank': break; // TODO
 				case 'EntExU2': break; // TODO
 				case 'SxView': break; // TODO
 				case 'Sxvd': break; // TODO
@@ -435,7 +448,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 				case 'Feat': break;
 				case 'FeatHdr': case 'FeatHdr11': break;
 				case 'Feature11': case 'Feature12': case 'List12': break;
-				case 'Blank': break;
 				case 'Country': country = val; break;
 				case 'RecalcId': break;
 				case 'DefaultRowHeight': case 'DxGCol': break; // TODO: htmlify
diff --git a/bits/80_parseods.js b/bits/80_parseods.js
index 42b01b3..b3b3c7f 100644
--- a/bits/80_parseods.js
+++ b/bits/80_parseods.js
@@ -58,8 +58,10 @@ var parse_content_xml = (function() {
 				rowtag = parsexmltag(Rn[0], false);
 				if(rowtag['行号']) R = rowtag['行号'] - 1; else ++R;
 				C = -1; break;
-			case 'covered-table-cell': // 9.1.5 table:covered-table-cell
-				++C; break; /* stub */
+			case 'covered-table-cell': // 9.1.5 <table:covered-table-cell>
+				++C;
+				if(opts.sheetStubs) ws[encode_cell({r:R,c:C})] = {t:'z'};
+				break; /* stub */
 			case 'table-cell': case '数据':
 				if(Rn[0].charAt(Rn[0].length-2) === '/') {
 					ctag = parsexmltag(Rn[0], false);
@@ -121,7 +123,7 @@ var parse_content_xml = (function() {
 						isstub = textpidx == 0;
 					}
 					if(textp) q.w = textp;
-					if(!isstub || opts.cellStubs) {
+					if(!isstub || opts.sheetStubs) {
 						if(!(opts.sheetRows && opts.sheetRows < R)) {
 							ws[encode_cell({r:R,c:C})] = q;
 							while(--rept > 0) ws[encode_cell({r:R,c:++C})] = dup(q);
diff --git a/bits/90_utils.js b/bits/90_utils.js
index 56f7c43..b1f3b89 100644
--- a/bits/90_utils.js
+++ b/bits/90_utils.js
@@ -66,7 +66,7 @@ function safe_format_cell(cell/*:Cell*/, v/*:any*/) {
 }
 
 function format_cell(cell/*:Cell*/, v/*:any*/) {
-	if(cell == null || cell.t == null) return "";
+	if(cell == null || cell.t == null || cell.t == 'z') return "";
 	if(cell.w !== undefined) return cell.w;
 	if(v === undefined) return safe_format_cell(cell, cell.v);
 	return safe_format_cell(cell, v);
@@ -121,6 +121,7 @@ function sheet_to_json(sheet/*:Worksheet*/, opts/*:?Sheet2JSONOpts*/){
 			if(val === undefined || val.t === undefined) continue;
 			v = val.v;
 			switch(val.t){
+				case 'z': continue;
 				case 'e': continue;
 				case 's': break;
 				case 'b': case 'n': break;
@@ -187,6 +188,7 @@ function sheet_to_formulae(sheet/*:Worksheet*/)/*:Array<string>*/ {
 				if(y.indexOf(":") == -1) y = y + ":" + y;
 			}
 			if(x.f != null) val = x.f;
+			else if(x.t == 'z') continue;
 			else if(x.t == 'n' && x.v != null) val = "" + x.v;
 			else if(x.t == 'b') val = x.v ? "TRUE" : "FALSE";
 			else if(x.w !== undefined) val = "'" + x.w;
diff --git a/test.js b/test.js
index 677b17b..9dfb1d2 100644
--- a/test.js
+++ b/test.js
@@ -346,10 +346,18 @@ describe('parse options', function() {
 			});
 		});
 		it('should generate sheet stubs when requested', function() {
-			/* TODO: ODS/XLS/XML */
-			[paths.mcxlsx, paths.mcxlsb /*, paths.mcods, paths.mcxls, paths.mcxml*/].forEach(function(p) {
+			[paths.mcxlsx, paths.mcxlsb, paths.mcods, paths.mcxls, paths.mcxml].forEach(function(p) {
 				var wb = X.readFile(p, {sheetStubs:true});
-				assert(typeof wb.Sheets.Merge.A2.t !== 'undefined');
+				assert(wb.Sheets.Merge.A2.t == 'z');
+			});
+		});
+		it('should handle stub cells', function() {
+			[paths.mcxlsx, paths.mcxlsb, paths.mcods, paths.mcxls, paths.mcxml].forEach(function(p) {
+				var wb = X.readFile(p, {sheetStubs:true});
+				X.utils.sheet_to_csv(wb.Sheets.Merge);
+				X.utils.sheet_to_json(wb.Sheets.Merge);
+				X.utils.sheet_to_formulae(wb.Sheets.Merge);
+				ofmt.forEach(function(f) { X.write(wb, {type:"binary", bookType:f}); });
 			});
 		});
 		function checkcells(wb, A46, B26, C16, D2) {
diff --git a/xlsx.flow.js b/xlsx.flow.js
index 5a1bd36..ec207bb 100644
--- a/xlsx.flow.js
+++ b/xlsx.flow.js
@@ -3852,6 +3852,17 @@ function parse_MulRk(blob, length) {
 	if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch";
 	return {r:rw, c:col, C:lastcol, rkrec:rkrecs};
 }
+/* 2.4.174 */
+function parse_MulBlank(blob, length) {
+	var target = blob.l + length - 2;
+	var rw = blob.read_shift(2), col = blob.read_shift(2);
+	var ixfes = [];
+	while(blob.l < target) ixfes.push(blob.read_shift(2));
+	if(blob.l !== target) throw "MulBlank read error";
+	var lastcol = blob.read_shift(2);
+	if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch";
+	return {r:rw, c:col, C:lastcol, ixfe:ixfes};
+}
 
 /* 2.5.20 2.5.249 TODO: interpret values here */
 function parse_CellStyleXF(blob, length, style) {
@@ -4248,7 +4259,6 @@ var parse_SXLI = parsenoop;
 var parse_SXPI = parsenoop;
 var parse_DocRoute = parsenoop;
 var parse_RecipName = parsenoop;
-var parse_MulBlank = parsenoop;
 var parse_SXDI = parsenoop;
 var parse_SXDB = parsenoop;
 var parse_SXFDB = parsenoop;
@@ -8219,6 +8229,7 @@ function get_cell_style(styles, cell, opts) {
 }
 
 function safe_format(p, fmtid, fillid, opts) {
+	if(p.t === 'z') return;
 	if(p.t === 'd' && typeof p.v === 'string') p.v = new Date(p.v);
 	try {
 		if(p.t === 'e') p.w = p.w || BErr[p.v];
@@ -8340,7 +8351,7 @@ function parse_ws_xml_hlinks(s, data, rels) {
 		var rng = safe_decode_range(val.ref);
 		for(var R=rng.s.r;R<=rng.e.r;++R) for(var C=rng.s.c;C<=rng.e.c;++C) {
 			var addr = encode_cell({c:C,r:R});
-			if(!s[addr]) s[addr] = {t:"stub",v:undefined};
+			if(!s[addr]) s[addr] = {t:"z",v:undefined};
 			s[addr].l = val;
 		}
 	}
@@ -8379,7 +8390,7 @@ function write_ws_xml_cols(ws, cols)/*:string*/ {
 }
 
 function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) {
-	if(cell.v === undefined) return "";
+	if(cell.v === undefined || cell.t === 'z') return "";
 	var vv = "";
 	var oldt = cell.t, oldv = cell.v;
 	switch(cell.t) {
@@ -8492,7 +8503,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 			/* SCHEMA IS ACTUALLY INCORRECT HERE.  IF A CELL HAS NO T, EMIT "" */
 			if(tag.t === undefined && p.v === undefined) {
 				if(!opts.sheetStubs) continue;
-				p.t = "stub";
+				p.t = "z";
 			}
 			else p.t = tag.t || "n";
 			if(guess.s.c > idx) guess.s.c = idx;
@@ -8504,7 +8515,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 					sstr = strs[parseInt(p.v, 10)];
 					if(typeof p.v == 'undefined') {
 						if(!opts.sheetStubs) continue;
-						p.t = "stub";
+						p.t = 'z';
 					}
 					p.v = sstr.t;
 					p.r = sstr.r;
@@ -8910,7 +8921,7 @@ function parse_ws_bin(data, opts, rels, wb)/*:Worksheet*/ {
 				break;
 
 			case 'BrtCellBlank': if(!opts.sheetStubs) break;
-				p = ({t:'s',v:undefined}/*:any*/);
+				p = ({t:'z',v:undefined}/*:any*/);
 				s[encode_col(C=val[0].c) + rr] = p;
 				if(refguess.s.r > row.r) refguess.s.r = row.r;
 				if(refguess.s.c > C) refguess.s.c = C;
@@ -9767,6 +9778,7 @@ function xlml_set_custprop(Custprops, Rn, cp, val/*:string*/) {
 }
 
 function safe_format_xlml(cell/*:Cell*/, nf, o) {
+	if(cell.t === 'z') return;
 	try {
 		if(cell.t === 'e') { cell.w = cell.w || BErr[cell.v]; }
 		else if(nf === "General") {
@@ -9909,8 +9921,17 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
 					var rr = r + (parseInt(cell.MergeDown,10)|0);
 					mergecells.push({s:{c:c,r:r},e:{c:cc,r:rr}});
 				}
-				++c;
-				if(cell.MergeAcross) c += +cell.MergeAcross;
+				if(!opts.sheetStubs) { if(cell.MergeAcross) c = cc + 1; else ++c; }
+				else if(cell.MergeAcross || cell.MergeDown) {
+					/*:: if(!cc) cc = 0; if(!rr) rr = 0; */
+					for(var cma = c; cma <= cc; ++cma) {
+						for(var cmd = r; cmd <= rr; ++cmd) {
+							if(cma > c || cmd > r) cursheet[encode_col(cma) + encode_row(cmd)] = {t:'z'};
+						}
+					}
+					c = cc + 1;
+				}
+				else ++c;
 			} else {
 				cell = xlml_parsexmltagobj(Rn[0]);
 				if(cell.Index) c = +cell.Index - 1;
@@ -10465,6 +10486,7 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{
 
 	var t = "", p = "";
 	switch(cell.t) {
+		case 'z': return "";
 		case 'n': t = 'Number'; p = String(cell.v); break;
 		case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break;
 		case 'e': t = 'Error'; p = BErr[cell.v]; break;
@@ -10574,6 +10596,7 @@ function slurp(R, blob, length/*:number*/, opts) {
 }
 
 function safe_format_xf(p/*:any*/, opts/*:ParseOpts*/, date1904/*:?boolean*/) {
+	if(p.t === 'z') return;
 	if(p.t === 'e') { p.w = p.w || BErr[p.v]; }
 	if(!p.XF) return;
 	try {
@@ -10875,6 +10898,19 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 					safe_format_xf(temp_val, options, wb.opts.Date1904);
 					addcell({c:val.c, r:val.r}, temp_val, options);
 					break;
+				case 'Blank': if(options.sheetStubs) {
+					temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], t:'z'};
+					safe_format_xf(temp_val, options, wb.opts.Date1904);
+					addcell({c:val.c, r:val.r}, temp_val, options);
+				} break;
+				case 'MulBlank': if(options.sheetStubs) {
+					for(var _j = val.c; _j <= val.C; ++_j) {
+						var _ixfe = val.ixfe[_j-val.c];
+						temp_val= {ixfe:_ixfe, XF:XFs[_ixfe], t:'z'};
+						safe_format_xf(temp_val, options, wb.opts.Date1904);
+						addcell({c:_j, r:val.r}, temp_val, options);
+					}
+				} break;
 				case 'RString':
 				case 'Label': case 'BIFF2STR':
 					temp_val=make_cell(val.val, val.ixfe, 's');
@@ -10941,7 +10977,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 				case 'ColInfo': break; // TODO
 				case 'Row': break; // TODO
 				case 'DBCell': break; // TODO
-				case 'MulBlank': break; // TODO
 				case 'EntExU2': break; // TODO
 				case 'SxView': break; // TODO
 				case 'Sxvd': break; // TODO
@@ -10956,7 +10991,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
 				case 'Feat': break;
 				case 'FeatHdr': case 'FeatHdr11': break;
 				case 'Feature11': case 'Feature12': case 'List12': break;
-				case 'Blank': break;
 				case 'Country': country = val; break;
 				case 'RecalcId': break;
 				case 'DefaultRowHeight': case 'DxGCol': break; // TODO: htmlify
@@ -12673,8 +12707,10 @@ var parse_content_xml = (function() {
 				rowtag = parsexmltag(Rn[0], false);
 				if(rowtag['行号']) R = rowtag['行号'] - 1; else ++R;
 				C = -1; break;
-			case 'covered-table-cell': // 9.1.5 table:covered-table-cell
-				++C; break; /* stub */
+			case 'covered-table-cell': // 9.1.5 <table:covered-table-cell>
+				++C;
+				if(opts.sheetStubs) ws[encode_cell({r:R,c:C})] = {t:'z'};
+				break; /* stub */
 			case 'table-cell': case '数据':
 				if(Rn[0].charAt(Rn[0].length-2) === '/') {
 					ctag = parsexmltag(Rn[0], false);
@@ -12736,7 +12772,7 @@ var parse_content_xml = (function() {
 						isstub = textpidx == 0;
 					}
 					if(textp) q.w = textp;
-					if(!isstub || opts.cellStubs) {
+					if(!isstub || opts.sheetStubs) {
 						if(!(opts.sheetRows && opts.sheetRows < R)) {
 							ws[encode_cell({r:R,c:C})] = q;
 							while(--rept > 0) ws[encode_cell({r:R,c:++C})] = dup(q);
@@ -13571,7 +13607,7 @@ function safe_format_cell(cell/*:Cell*/, v/*:any*/) {
 }
 
 function format_cell(cell/*:Cell*/, v/*:any*/) {
-	if(cell == null || cell.t == null) return "";
+	if(cell == null || cell.t == null || cell.t == 'z') return "";
 	if(cell.w !== undefined) return cell.w;
 	if(v === undefined) return safe_format_cell(cell, cell.v);
 	return safe_format_cell(cell, v);
@@ -13626,6 +13662,7 @@ function sheet_to_json(sheet/*:Worksheet*/, opts/*:?Sheet2JSONOpts*/){
 			if(val === undefined || val.t === undefined) continue;
 			v = val.v;
 			switch(val.t){
+				case 'z': continue;
 				case 'e': continue;
 				case 's': break;
 				case 'b': case 'n': break;
@@ -13692,6 +13729,7 @@ function sheet_to_formulae(sheet/*:Worksheet*/)/*:Array<string>*/ {
 				if(y.indexOf(":") == -1) y = y + ":" + y;
 			}
 			if(x.f != null) val = x.f;
+			else if(x.t == 'z') continue;
 			else if(x.t == 'n' && x.v != null) val = "" + x.v;
 			else if(x.t == 'b') val = x.v ? "TRUE" : "FALSE";
 			else if(x.w !== undefined) val = "'" + x.w;
diff --git a/xlsx.js b/xlsx.js
index b82edba..659dbfd 100644
--- a/xlsx.js
+++ b/xlsx.js
@@ -3800,6 +3800,17 @@ function parse_MulRk(blob, length) {
 	if(rkrecs.length != lastcol - col + 1) throw "MulRK length mismatch";
 	return {r:rw, c:col, C:lastcol, rkrec:rkrecs};
 }
+/* 2.4.174 */
+function parse_MulBlank(blob, length) {
+	var target = blob.l + length - 2;
+	var rw = blob.read_shift(2), col = blob.read_shift(2);
+	var ixfes = [];
+	while(blob.l < target) ixfes.push(blob.read_shift(2));
+	if(blob.l !== target) throw "MulBlank read error";
+	var lastcol = blob.read_shift(2);
+	if(ixfes.length != lastcol - col + 1) throw "MulBlank length mismatch";
+	return {r:rw, c:col, C:lastcol, ixfe:ixfes};
+}
 
 /* 2.5.20 2.5.249 TODO: interpret values here */
 function parse_CellStyleXF(blob, length, style) {
@@ -4196,7 +4207,6 @@ var parse_SXLI = parsenoop;
 var parse_SXPI = parsenoop;
 var parse_DocRoute = parsenoop;
 var parse_RecipName = parsenoop;
-var parse_MulBlank = parsenoop;
 var parse_SXDI = parsenoop;
 var parse_SXDB = parsenoop;
 var parse_SXFDB = parsenoop;
@@ -8166,6 +8176,7 @@ function get_cell_style(styles, cell, opts) {
 }
 
 function safe_format(p, fmtid, fillid, opts) {
+	if(p.t === 'z') return;
 	if(p.t === 'd' && typeof p.v === 'string') p.v = new Date(p.v);
 	try {
 		if(p.t === 'e') p.w = p.w || BErr[p.v];
@@ -8287,7 +8298,7 @@ function parse_ws_xml_hlinks(s, data, rels) {
 		var rng = safe_decode_range(val.ref);
 		for(var R=rng.s.r;R<=rng.e.r;++R) for(var C=rng.s.c;C<=rng.e.c;++C) {
 			var addr = encode_cell({c:C,r:R});
-			if(!s[addr]) s[addr] = {t:"stub",v:undefined};
+			if(!s[addr]) s[addr] = {t:"z",v:undefined};
 			s[addr].l = val;
 		}
 	}
@@ -8326,7 +8337,7 @@ function write_ws_xml_cols(ws, cols) {
 }
 
 function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) {
-	if(cell.v === undefined) return "";
+	if(cell.v === undefined || cell.t === 'z') return "";
 	var vv = "";
 	var oldt = cell.t, oldv = cell.v;
 	switch(cell.t) {
@@ -8439,7 +8450,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 			/* SCHEMA IS ACTUALLY INCORRECT HERE.  IF A CELL HAS NO T, EMIT "" */
 			if(tag.t === undefined && p.v === undefined) {
 				if(!opts.sheetStubs) continue;
-				p.t = "stub";
+				p.t = "z";
 			}
 			else p.t = tag.t || "n";
 			if(guess.s.c > idx) guess.s.c = idx;
@@ -8451,7 +8462,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess) {
 					sstr = strs[parseInt(p.v, 10)];
 					if(typeof p.v == 'undefined') {
 						if(!opts.sheetStubs) continue;
-						p.t = "stub";
+						p.t = 'z';
 					}
 					p.v = sstr.t;
 					p.r = sstr.r;
@@ -8857,7 +8868,7 @@ function parse_ws_bin(data, opts, rels, wb) {
 				break;
 
 			case 'BrtCellBlank': if(!opts.sheetStubs) break;
-				p = ({t:'s',v:undefined});
+				p = ({t:'z',v:undefined});
 				s[encode_col(C=val[0].c) + rr] = p;
 				if(refguess.s.r > row.r) refguess.s.r = row.r;
 				if(refguess.s.c > C) refguess.s.c = C;
@@ -9712,6 +9723,7 @@ function xlml_set_custprop(Custprops, Rn, cp, val) {
 }
 
 function safe_format_xlml(cell, nf, o) {
+	if(cell.t === 'z') return;
 	try {
 		if(cell.t === 'e') { cell.w = cell.w || BErr[cell.v]; }
 		else if(nf === "General") {
@@ -9854,8 +9866,16 @@ function parse_xlml_xml(d, opts) {
 					var rr = r + (parseInt(cell.MergeDown,10)|0);
 					mergecells.push({s:{c:c,r:r},e:{c:cc,r:rr}});
 				}
-				++c;
-				if(cell.MergeAcross) c += +cell.MergeAcross;
+				if(!opts.sheetStubs) { if(cell.MergeAcross) c = cc + 1; else ++c; }
+				else if(cell.MergeAcross || cell.MergeDown) {
+for(var cma = c; cma <= cc; ++cma) {
+						for(var cmd = r; cmd <= rr; ++cmd) {
+							if(cma > c || cmd > r) cursheet[encode_col(cma) + encode_row(cmd)] = {t:'z'};
+						}
+					}
+					c = cc + 1;
+				}
+				else ++c;
 			} else {
 				cell = xlml_parsexmltagobj(Rn[0]);
 				if(cell.Index) c = +cell.Index - 1;
@@ -10409,6 +10429,7 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){
 
 	var t = "", p = "";
 	switch(cell.t) {
+		case 'z': return "";
 		case 'n': t = 'Number'; p = String(cell.v); break;
 		case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break;
 		case 'e': t = 'Error'; p = BErr[cell.v]; break;
@@ -10518,6 +10539,7 @@ function slurp(R, blob, length, opts) {
 }
 
 function safe_format_xf(p, opts, date1904) {
+	if(p.t === 'z') return;
 	if(p.t === 'e') { p.w = p.w || BErr[p.v]; }
 	if(!p.XF) return;
 	try {
@@ -10819,6 +10841,19 @@ function parse_workbook(blob, options) {
 					safe_format_xf(temp_val, options, wb.opts.Date1904);
 					addcell({c:val.c, r:val.r}, temp_val, options);
 					break;
+				case 'Blank': if(options.sheetStubs) {
+					temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], t:'z'};
+					safe_format_xf(temp_val, options, wb.opts.Date1904);
+					addcell({c:val.c, r:val.r}, temp_val, options);
+				} break;
+				case 'MulBlank': if(options.sheetStubs) {
+					for(var _j = val.c; _j <= val.C; ++_j) {
+						var _ixfe = val.ixfe[_j-val.c];
+						temp_val= {ixfe:_ixfe, XF:XFs[_ixfe], t:'z'};
+						safe_format_xf(temp_val, options, wb.opts.Date1904);
+						addcell({c:_j, r:val.r}, temp_val, options);
+					}
+				} break;
 				case 'RString':
 				case 'Label': case 'BIFF2STR':
 					temp_val=make_cell(val.val, val.ixfe, 's');
@@ -10885,7 +10920,6 @@ function parse_workbook(blob, options) {
 				case 'ColInfo': break; // TODO
 				case 'Row': break; // TODO
 				case 'DBCell': break; // TODO
-				case 'MulBlank': break; // TODO
 				case 'EntExU2': break; // TODO
 				case 'SxView': break; // TODO
 				case 'Sxvd': break; // TODO
@@ -10900,7 +10934,6 @@ function parse_workbook(blob, options) {
 				case 'Feat': break;
 				case 'FeatHdr': case 'FeatHdr11': break;
 				case 'Feature11': case 'Feature12': case 'List12': break;
-				case 'Blank': break;
 				case 'Country': country = val; break;
 				case 'RecalcId': break;
 				case 'DefaultRowHeight': case 'DxGCol': break; // TODO: htmlify
@@ -12617,8 +12650,10 @@ var parse_content_xml = (function() {
 				rowtag = parsexmltag(Rn[0], false);
 				if(rowtag['行号']) R = rowtag['行号'] - 1; else ++R;
 				C = -1; break;
-			case 'covered-table-cell': // 9.1.5 table:covered-table-cell
-				++C; break; /* stub */
+			case 'covered-table-cell': // 9.1.5 <table:covered-table-cell>
+				++C;
+				if(opts.sheetStubs) ws[encode_cell({r:R,c:C})] = {t:'z'};
+				break; /* stub */
 			case 'table-cell': case '数据':
 				if(Rn[0].charAt(Rn[0].length-2) === '/') {
 					ctag = parsexmltag(Rn[0], false);
@@ -12680,7 +12715,7 @@ var parse_content_xml = (function() {
 						isstub = textpidx == 0;
 					}
 					if(textp) q.w = textp;
-					if(!isstub || opts.cellStubs) {
+					if(!isstub || opts.sheetStubs) {
 						if(!(opts.sheetRows && opts.sheetRows < R)) {
 							ws[encode_cell({r:R,c:C})] = q;
 							while(--rept > 0) ws[encode_cell({r:R,c:++C})] = dup(q);
@@ -13507,7 +13542,7 @@ function safe_format_cell(cell, v) {
 }
 
 function format_cell(cell, v) {
-	if(cell == null || cell.t == null) return "";
+	if(cell == null || cell.t == null || cell.t == 'z') return "";
 	if(cell.w !== undefined) return cell.w;
 	if(v === undefined) return safe_format_cell(cell, cell.v);
 	return safe_format_cell(cell, v);
@@ -13562,6 +13597,7 @@ function sheet_to_json(sheet, opts){
 			if(val === undefined || val.t === undefined) continue;
 			v = val.v;
 			switch(val.t){
+				case 'z': continue;
 				case 'e': continue;
 				case 's': break;
 				case 'b': case 'n': break;
@@ -13628,6 +13664,7 @@ function sheet_to_formulae(sheet) {
 				if(y.indexOf(":") == -1) y = y + ":" + y;
 			}
 			if(x.f != null) val = x.f;
+			else if(x.t == 'z') continue;
 			else if(x.t == 'n' && x.v != null) val = "" + x.v;
 			else if(x.t == 'b') val = x.v ? "TRUE" : "FALSE";
 			else if(x.w !== undefined) val = "'" + x.w;