Skip to content

Commit dcee744

Browse files
committed
row and column size and visibility
- XLSX/XLSB/XLS/XLML/SYLK rows and columns - corrected pixel/point calculations using PPI - XLSX/XLSB generate sheet view - clarified sheet protection default behavior - fixed eslintrc semi check
1 parent c6f96c3 commit dcee744

20 files changed

+999
-235
lines changed

.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"curly": 0,
1414
"comma-style": [ 2, "last" ],
1515
"no-trailing-spaces": 2,
16+
"semi": [ 2, "always" ],
1617
"comma-dangle": [ 2, "never" ]
1718
}
1819
}

README.md

+74-24
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ enhancements and additional features by request.
5757
* [Document Features](#document-features)
5858
+ [Formulae](#formulae)
5959
+ [Column Properties](#column-properties)
60+
+ [Row Properties](#row-properties)
6061
+ [Hyperlinks](#hyperlinks)
6162
+ [Cell Comments](#cell-comments)
6263
+ [Sheet Visibility](#sheet-visibility)
@@ -97,6 +98,7 @@ enhancements and additional features by request.
9798
* [Tested Environments](#tested-environments)
9899
* [Test Files](#test-files)
99100
- [Contributing](#contributing)
101+
* [Tests](#tests)
100102
* [OSX/Linux](#osxlinux)
101103
* [Windows](#windows)
102104
- [License](#license)
@@ -630,33 +632,37 @@ In addition to the base sheet keys, worksheets also add:
630632
parsed, the column objects store the pixel width in the `wpx` field, character
631633
width in the `wch` field, and the maximum digit width in the `MDW` field.
632634

635+
- `ws['!rows']`: array of row properties objects as explained later in the docs.
636+
Each row object encodes properties including row height and visibility.
637+
633638
- `ws['!merges']`: array of range objects corresponding to the merged cells in
634639
the worksheet. Plaintext utilities are unaware of merge cells. CSV export
635640
will write all cells in the merge range if they exist, so be sure that only
636641
the first cell (upper-left) in the range is set.
637642

638-
- `ws['protect']`: object of write sheet protection properties. The `password`
643+
- `ws['!protect']`: object of write sheet protection properties. The `password`
639644
key specifies the password for formats that support password-protected sheets
640645
(XLSX/XLSB/XLS). The writer uses the XOR obfuscation method. The following
641-
keys control the sheet protection (same as ECMA-376 18.3.1.85):
642-
643-
| key | functionality disabled if value is true |
644-
|:----------------------|:-----------------------------------------------------|
645-
| `selectLockedCells` | Select locked cells |
646-
| `selectUnlockedCells` | Select unlocked cells |
647-
| `formatCells` | Format cells |
648-
| `formatColumns` | Format columns |
649-
| `formatRows` | Format rows |
650-
| `insertColumns` | Insert columns |
651-
| `insertRows` | Insert rows |
652-
| `insertHyperlinks` | Insert hyperlinks |
653-
| `deleteColumns` | Delete columns |
654-
| `deleteRows` | Delete rows |
655-
| `sort` | Sort |
656-
| `autoFilter` | Filter |
657-
| `pivotTables` | Use PivotTable reports |
658-
| `objects` | Edit objects |
659-
| `scenarios` | Edit scenarios |
646+
keys control the sheet protection -- set to `false` to enable a feature when
647+
sheet is locked or set to `true` to disable a feature:
648+
649+
| key | feature (true=disabled / false=enabled) | default |
650+
|:----------------------|:----------------------------------------|:-----------|
651+
| `selectLockedCells` | Select locked cells | enabled |
652+
| `selectUnlockedCells` | Select unlocked cells | enabled |
653+
| `formatCells` | Format cells | disabled |
654+
| `formatColumns` | Format columns | disabled |
655+
| `formatRows` | Format rows | disabled |
656+
| `insertColumns` | Insert columns | disabled |
657+
| `insertRows` | Insert rows | disabled |
658+
| `insertHyperlinks` | Insert hyperlinks | disabled |
659+
| `deleteColumns` | Delete columns | disabled |
660+
| `deleteRows` | Delete rows | disabled |
661+
| `sort` | Sort | disabled |
662+
| `autoFilter` | Filter | disabled |
663+
| `pivotTables` | Use PivotTable reports | disabled |
664+
| `objects` | Edit objects | enabled |
665+
| `scenarios` | Edit scenarios | enabled |
660666

661667
- `ws['!autofilter']`: AutoFilter object following the schema:
662668

@@ -835,6 +841,7 @@ Since Excel prohibits named cells from colliding with names of A1 or RC style
835841
cell references, a (not-so-simple) regex conversion is possible. BIFF Parsed
836842
formulae have to be explicitly unwound. OpenFormula formulae can be converted
837843
with regexes for the most part.
844+
838845
#### Column Properties
839846

840847
Excel internally stores column widths in a nebulous "Max Digit Width" form. The
@@ -853,10 +860,11 @@ objects which have the following properties:
853860

854861
```typescript
855862
type ColInfo = {
856-
MDW?:number; // Excel's "Max Digit Width" unit, always integral
857-
width:number; // width in Excel's "Max Digit Width", width*256 is integral
858-
wpx?:number; // width in screen pixels
859-
wch?:number; // intermediate character calculation
863+
MDW?:number; // Excel's "Max Digit Width" unit, always integral
864+
width:number; // width in Excel's "Max Digit Width", width*256 is integral
865+
wpx?:number; // width in screen pixels
866+
wch?:number; // intermediate character calculation
867+
hidden:?boolean; // if true, the column is hidden
860868
};
861869
```
862870

@@ -867,6 +875,29 @@ follow the priority order:
867875
2) use `wpx` pixel width if available
868876
3) use `wch` character count if available
869877

878+
#### Row Properties
879+
880+
Excel internally stores row heights in points. The default resolution is 72 DPI
881+
or 96 PPI, so the pixel and point size should agree. For different resolutions
882+
they may not agree, so the library separates the concepts.
883+
884+
The `!rows` array in each worksheet, if present, is a collection of `RowInfo`
885+
objects which have the following properties:
886+
887+
```typescript
888+
type RowInfo = {
889+
hpx?:number; // height in screen pixels
890+
hpt?:number; // height in points
891+
hidden:?boolean; // if true, the row is hidden
892+
};
893+
```
894+
895+
Even though all of the information is made available, writers are expected to
896+
follow the priority order:
897+
898+
1) use `hpx` pixel height if available
899+
2) use `hpt` point height if available
900+
870901
#### Hyperlinks
871902

872903
Hyperlinks are stored in the `l` key of cell objects. The `Target` field of the
@@ -1520,6 +1551,25 @@ Running `make init` will refresh the `test_files` submodule and get the files.
15201551
Due to the precarious nature of the Open Specifications Promise, it is very
15211552
important to ensure code is cleanroom. Consult CONTRIBUTING.md
15221553

1554+
### Tests
1555+
1556+
The `test_misc` target (`make test_misc` on Linux/OSX / `make misc` on Windows)
1557+
runs the targeted feature tests. It should take 5-10 seconds to perform feature
1558+
tests without testing against the entire test battery. New features should be
1559+
accompanied with tests for the relevant file formats and features.
1560+
1561+
For tests involving the read side, an appropriate feature test would involve
1562+
reading an existing file and checking the resulting workbook object. If a
1563+
parameter is involved, files should be read with different values for the param
1564+
to verify that the feature is working as expected.
1565+
1566+
For tests involving a new write feature which can already be parsed, appropriate
1567+
feature tests would involve writing a workbook with the feature and then opening
1568+
and verifying that the feature is preserved.
1569+
1570+
For tests involving a new write feature without an existing read ability, please
1571+
add a feature test to the kitchen sink `tests/write.js`.
1572+
15231573
### OSX/Linux
15241574

15251575
The xlsx.js file is constructed from the files in the `bits` subdirectory. The

bits/39_xlsbiff.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,19 @@ function parse_ExtSST(blob, length) {
210210
}
211211

212212

213-
/* 2.4.221 TODO*/
213+
/* 2.4.221 TODO: check BIFF2-4 */
214214
function parse_Row(blob, length) {
215-
var rw = blob.read_shift(2), col = blob.read_shift(2), Col = blob.read_shift(2), rht = blob.read_shift(2);
216-
blob.read_shift(4); // reserved(2), unused(2)
215+
var z = ({}/*:any*/);
216+
z.r = blob.read_shift(2);
217+
z.c = blob.read_shift(2);
218+
z.cnt = blob.read_shift(2) - z.c;
219+
var miyRw = blob.read_shift(2);
220+
blob.l += 4; // reserved(2), unused(2)
217221
var flags = blob.read_shift(1); // various flags
218-
blob.read_shift(1); // reserved
219-
blob.read_shift(2); //ixfe, other flags
220-
return {r:rw, c:col, cnt:Col-col};
222+
blob.l += 3; // reserved(8), ixfe(12), flags(4)
223+
if(flags & 0x20) z.hidden = true;
224+
if(flags & 0x40) z.hpt = miyRw / 20;
225+
return z;
221226
}
222227

223228

bits/40_harb.js

+65-13
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,19 @@ var SYLK = (function() {
206206
var records = str.split(/[\n\r]+/), R = -1, C = -1, ri = 0, rj = 0, arr = [];
207207
var formats = [];
208208
var next_cell_format = null;
209+
var sht = {}, rowinfo = [], colinfo = [], cw = [];
210+
var Mval = 0, j;
209211
for (; ri !== records.length; ++ri) {
212+
Mval = 0;
210213
var record = records[ri].trim().split(";");
211214
var RT = record[0], val;
212-
if(RT === 'P') for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
213-
case 'P':
214-
formats.push(record[rj].substr(1));
215-
break;
216-
}
217-
else if(RT !== 'C' && RT !== 'F') continue;
218-
else for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
215+
switch(RT) {
216+
case 'P': if(record[1].charAt(0) == 'P') formats.push(records[ri].trim().substr(3).replace(/;;/g, ";"));
217+
break;
218+
case 'C': case 'F': for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
219219
case 'Y':
220220
R = parseInt(record[rj].substr(1))-1; C = 0;
221-
for(var j = arr.length; j <= R; ++j) arr[j] = [];
221+
for(j = arr.length; j <= R; ++j) arr[j] = [];
222222
break;
223223
case 'X': C = parseInt(record[rj].substr(1))-1; break;
224224
case 'K':
@@ -228,20 +228,45 @@ var SYLK = (function() {
228228
else if(val === 'FALSE') val = false;
229229
else if(+val === +val) {
230230
val = +val;
231-
if(next_cell_format !== null && next_cell_format.match(/[ymdhmsYMDHMS]/)) val = numdate(val);
231+
if(next_cell_format !== null && SSF.is_date(next_cell_format)) val = numdate(val);
232232
}
233233
arr[R][C] = val;
234234
next_cell_format = null;
235235
break;
236236
case 'P':
237237
if(RT !== 'F') break;
238238
next_cell_format = formats[parseInt(record[rj].substr(1))];
239+
break;
240+
case 'M': Mval = parseInt(record[rj].substr(1)) / 20; break;
241+
case 'W':
242+
if(RT !== 'F') break;
243+
cw = record[rj].substr(1).split(" ");
244+
for(j = parseInt(cw[0], 10); j <= parseInt(cw[1], 10); ++j) {
245+
Mval = parseInt(cw[2], 10);
246+
colinfo[j-1] = Mval == 0 ? {hidden:true}: {wch:Mval}; process_col(colinfo[j-1]);
247+
} break;
248+
case 'R':
249+
R = parseInt(record[rj].substr(1))-1;
250+
rowinfo[R] = {};
251+
if(Mval > 0) { rowinfo[R].hpt = Mval; rowinfo[R].hpx = pt2px(Mval); }
252+
else if(Mval == 0) rowinfo[R].hidden = true;
253+
} break;
254+
default: break;
239255
}
240256
}
257+
if(rowinfo.length > 0) sht['!rows'] = rowinfo;
258+
if(colinfo.length > 0) sht['!cols'] = colinfo;
259+
arr[arr.length] = sht;
241260
return arr;
242261
}
243262

244-
function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(sylk_to_aoa(str, opts), opts); }
263+
function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ {
264+
var aoa = sylk_to_aoa(str, opts);
265+
var ws = aoa.pop();
266+
var o = aoa_to_sheet(aoa, opts);
267+
keys(ws).forEach(function(k) { o[k] = ws[k]; });
268+
return o;
269+
}
245270

246271
function sylk_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(sylk_to_sheet(str, opts), opts); }
247272

@@ -257,11 +282,40 @@ var SYLK = (function() {
257282
return o;
258283
}
259284

285+
function write_ws_cols_sylk(out, cols) {
286+
cols.forEach(function(col, i) {
287+
var rec = "F;W" + (i+1) + " " + (i+1) + " ";
288+
if(col.hidden) rec += "0";
289+
else {
290+
if(typeof col.width == 'number') col.wpx = width2px(col.width);
291+
if(typeof col.wpx == 'number') col.wch = px2char(col.wpx);
292+
if(typeof col.wch == 'number') rec += Math.round(col.wch);
293+
}
294+
if(rec.charAt(rec.length - 1) != " ") out.push(rec);
295+
});
296+
}
297+
298+
function write_ws_rows_sylk(out, rows) {
299+
rows.forEach(function(row, i) {
300+
var rec = "F;";
301+
if(row.hidden) rec += "M0;";
302+
else if(row.hpt) rec += "M" + 20 * row.hpt + ";";
303+
else if(row.hpx) rec += "M" + 20 * px2pt(row.hpx) + ";";
304+
if(rec.length > 2) out.push(rec + "R" + (i+1));
305+
});
306+
}
307+
260308
function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/)/*:string*/ {
261309
var preamble/*:Array<string>*/ = ["ID;PWXL;N;E"], o/*:Array<string>*/ = [];
262-
preamble.push("P;PGeneral");
263310
var r = decode_range(ws['!ref']), cell/*:Cell*/;
264311
var dense = Array.isArray(ws);
312+
var RS = "\r\n";
313+
314+
preamble.push("P;PGeneral");
315+
preamble.push("F;P0;DG0G8;M255");
316+
if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']);
317+
if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']);
318+
265319
for(var R = r.s.r; R <= r.e.r; ++R) {
266320
for(var C = r.s.c; C <= r.e.c; ++C) {
267321
var coord = encode_cell({r:R,c:C});
@@ -270,8 +324,6 @@ var SYLK = (function() {
270324
o.push(write_ws_cell_sylk(cell, ws, R, C, opts));
271325
}
272326
}
273-
preamble.push("F;P0;DG0G8;M255");
274-
var RS = "\r\n";
275327
return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS;
276328
}
277329

bits/45_styutils.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,17 @@ function process_col(coll/*:ColInfo*/) {
8585
coll.wch = px2char(coll.wpx);
8686
coll.width = char2width(coll.wch);
8787
coll.MDW = MDW;
88+
} else if(typeof coll.wch == 'number') {
89+
coll.width = char2width(coll.wch);
90+
coll.wpx = width2px(coll.width);
91+
coll.MDW = MDW;
8892
}
8993
if(coll.customWidth) delete coll.customWidth;
9094
}
9195

92-
var DEF_DPI = 96, DPI = DEF_DPI;
93-
function px2pt(px) { return px * 72 / DPI; }
94-
function pt2px(pt) { return pt * DPI / 72; }
96+
var DEF_PPI = 96, PPI = DEF_PPI;
97+
function px2pt(px) { return px * 96 / PPI; }
98+
function pt2px(pt) { return pt * PPI / 96; }
9599

96100
/* [MS-EXSPXML3] 2.4.54 ST_enmPattern */
97101
var XLMLPatternTypeMap = {

bits/66_wscommon.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ {
1414
function col_obj_w(C/*:number*/, col) {
1515
var p = ({min:C+1,max:C+1}/*:any*/);
1616
/* wch (chars), wpx (pixels) */
17-
var width = -1;
17+
var wch = -1;
1818
if(col.MDW) MDW = col.MDW;
1919
if(col.width != null) p.customWidth = 1;
20-
else if(col.wpx != null) width = px2char(col.wpx);
21-
else if(col.wch != null) width = col.wch;
22-
if(width > -1) { p.width = char2width(width); p.customWidth = 1; }
23-
else p.width = col.width;
20+
else if(col.wpx != null) wch = px2char(col.wpx);
21+
else if(col.wch != null) wch = col.wch;
22+
if(wch > -1) { p.width = char2width(wch); p.customWidth = 1; }
23+
else if(col.width != null) p.width = col.width;
24+
if(col.hidden) p.hidden = true;
2425
return p;
2526
}
2627

0 commit comments

Comments
 (0)