exceljs-lts 4.5.3 → 4.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +43 -11
  2. package/README_zh.md +29 -10
  3. package/dist/es5/doc/cell.js +37 -12
  4. package/dist/es5/doc/cell.js.map +1 -1
  5. package/dist/es5/doc/data-validations.js +94 -1
  6. package/dist/es5/doc/data-validations.js.map +1 -1
  7. package/dist/es5/doc/image.js +28 -0
  8. package/dist/es5/doc/image.js.map +1 -1
  9. package/dist/es5/doc/worksheet.js +8 -0
  10. package/dist/es5/doc/worksheet.js.map +1 -1
  11. package/dist/es5/stream/xlsx/workbook-reader.js +2 -1
  12. package/dist/es5/stream/xlsx/workbook-reader.js.map +1 -1
  13. package/dist/es5/stream/xlsx/workbook-writer.js +21 -1
  14. package/dist/es5/stream/xlsx/workbook-writer.js.map +1 -1
  15. package/dist/es5/stream/xlsx/worksheet-reader.js +1 -1
  16. package/dist/es5/stream/xlsx/worksheet-reader.js.map +1 -1
  17. package/dist/es5/stream/xlsx/worksheet-writer.js +89 -0
  18. package/dist/es5/stream/xlsx/worksheet-writer.js.map +1 -1
  19. package/dist/es5/xlsx/rel-type.js +1 -0
  20. package/dist/es5/xlsx/rel-type.js.map +1 -1
  21. package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js +53 -5
  22. package/dist/es5/xlsx/xform/sheet/auto-filter-xform.js.map +1 -1
  23. package/dist/es5/xlsx/xform/sheet/data-validations-xform.js +14 -3
  24. package/dist/es5/xlsx/xform/sheet/data-validations-xform.js.map +1 -1
  25. package/dist/exceljs.bare.js +237 -23
  26. package/dist/exceljs.bare.js.map +8 -8
  27. package/dist/exceljs.bare.min.js +6 -6
  28. package/dist/exceljs.bare.min.js.map +1 -1
  29. package/dist/exceljs.js +237 -23
  30. package/dist/exceljs.js.map +8 -8
  31. package/dist/exceljs.min.js +5 -5
  32. package/dist/exceljs.min.js.map +1 -1
  33. package/index.d.ts +40 -3
  34. package/lib/doc/cell.js +37 -12
  35. package/lib/doc/data-validations.js +83 -1
  36. package/lib/doc/image.js +32 -0
  37. package/lib/doc/worksheet.js +9 -0
  38. package/lib/stream/xlsx/workbook-reader.js +6 -1
  39. package/lib/stream/xlsx/workbook-writer.js +19 -0
  40. package/lib/stream/xlsx/worksheet-reader.js +1 -1
  41. package/lib/stream/xlsx/worksheet-writer.js +73 -0
  42. package/lib/xlsx/rel-type.js +1 -0
  43. package/lib/xlsx/xform/sheet/auto-filter-xform.js +42 -3
  44. package/lib/xlsx/xform/sheet/data-validations-xform.js +14 -3
  45. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -1,4 +1,24 @@
1
- declare interface Buffer extends ArrayBuffer { }
1
+ /**
2
+ * Node's Buffer type when @types/node is loaded, otherwise never.
3
+ * Read through `typeof globalThis` so these typings do not require @types/node.
4
+ * Newer @types/node declare `var Buffer`; older ones (class Buffer) expose it as `global.Buffer`.
5
+ */
6
+ type NodeBufferType = typeof globalThis extends { Buffer: { alloc(size: number): infer B } }
7
+ ? B
8
+ : typeof globalThis extends { global: { Buffer: { alloc(size: number): infer B } } }
9
+ ? B
10
+ : never;
11
+
12
+ /**
13
+ * Binary data returned by writeBuffer(): a Node Buffer when @types/node is loaded
14
+ * (which is what Node returns at runtime), otherwise ArrayBuffer.
15
+ */
16
+ declare type Buffer = [NodeBufferType] extends [never] ? ArrayBuffer : NodeBufferType;
17
+
18
+ /**
19
+ * Binary data accepted by xlsx.load().
20
+ */
21
+ type BufferInput = Buffer | ArrayBuffer | Uint8Array;
2
22
 
3
23
  export declare enum RelationshipType {
4
24
  None = 0,
@@ -830,10 +850,14 @@ export interface PageSetup {
830
850
  export interface HeaderFooter {
831
851
  /**
832
852
  * Set the value of differentFirst as true, which indicates that headers/footers for first page are different from the other pages, `false` by default
853
+ *
854
+ * Must be `true` for `firstHeader`/`firstFooter` to take effect.
833
855
  */
834
856
  differentFirst: boolean,
835
857
  /**
836
858
  * Set the value of differentOddEven as true, which indicates that headers/footers for odd and even pages are different, `false` by default
859
+ *
860
+ * Must be `true` for `evenHeader`/`evenFooter` to take effect.
837
861
  */
838
862
  differentOddEven: boolean,
839
863
  /**
@@ -846,18 +870,26 @@ export interface HeaderFooter {
846
870
  oddFooter: string,
847
871
  /**
848
872
  * Set header string for even pages, could format the string and `null` by default
873
+ *
874
+ * Only used when `differentOddEven` is `true`; otherwise Excel ignores it.
849
875
  */
850
876
  evenHeader: string,
851
877
  /**
852
878
  * Set footer string for even pages, could format the string and `null` by default
879
+ *
880
+ * Only used when `differentOddEven` is `true`; otherwise Excel ignores it.
853
881
  */
854
882
  evenFooter: string,
855
883
  /**
856
884
  * Set header string for the first page, could format the string and `null` by default
885
+ *
886
+ * Only used when `differentFirst` is `true`; otherwise Excel ignores it.
857
887
  */
858
888
  firstHeader: string,
859
889
  /**
860
890
  * Set footer string for the first page, could format the string and `null` by default
891
+ *
892
+ * Only used when `differentFirst` is `true`; otherwise Excel ignores it.
861
893
  */
862
894
  firstFooter: string
863
895
  }
@@ -865,6 +897,10 @@ export interface HeaderFooter {
865
897
  export type AutoFilter = string | {
866
898
  from: string | { row: number; column: number };
867
899
  to: string | { row: number; column: number };
900
+ /**
901
+ * Columns (0 = leftmost column of the range) whose filter button is hidden
902
+ */
903
+ exclude?: number[];
868
904
  };
869
905
 
870
906
  export interface WorksheetProtection {
@@ -889,7 +925,7 @@ export interface Image {
889
925
  extension: 'jpeg' | 'png' | 'gif';
890
926
  base64?: string;
891
927
  filename?: string;
892
- buffer?: Buffer;
928
+ buffer?: Buffer | ArrayBuffer;
893
929
  }
894
930
  export interface IAnchor {
895
931
  col: number;
@@ -1494,7 +1530,7 @@ export interface Xlsx {
1494
1530
  * load from an array buffer
1495
1531
  * @param buffer
1496
1532
  */
1497
- load(buffer: Buffer, options?: Partial<XlsxReadOptions>): Promise<Workbook>;
1533
+ load(buffer: BufferInput, options?: Partial<XlsxReadOptions>): Promise<Workbook>;
1498
1534
 
1499
1535
  /**
1500
1536
  * write to a buffer
@@ -1989,6 +2025,7 @@ export namespace stream {
1989
2025
  addThemes(): Promise<void>;
1990
2026
  addOfficeRels(): Promise<void>;
1991
2027
  addContentTypes(): Promise<void>;
2028
+ addDrawings(): Promise<void>;
1992
2029
  addApp(): Promise<void>;
1993
2030
  addCore(): Promise<void>;
1994
2031
  addSharedStrings(): Promise<void>;
package/lib/doc/cell.js CHANGED
@@ -4,6 +4,7 @@ const _ = require('../utils/under-dash');
4
4
  const Enums = require('./enums');
5
5
  const {slideFormula} = require('../utils/shared-formula');
6
6
  const Note = require('./note');
7
+ const {copyStyle} = require('../utils/copy-style');
7
8
  // Cell requirements
8
9
  // Operate inside a worksheet
9
10
  // Store and retrieve a value with a range of types: text, number, date, hyperlink, reference, formula, etc.
@@ -39,7 +40,8 @@ class Cell {
39
40
 
40
41
  // help GC by removing cyclic (and other) references
41
42
  destroy() {
42
- delete this.style;
43
+ delete this._style;
44
+ delete this._styleShared;
43
45
  delete this._value;
44
46
  delete this._row;
45
47
  delete this._column;
@@ -48,8 +50,31 @@ class Cell {
48
50
 
49
51
  // =========================================================================
50
52
  // Styles stuff
53
+
54
+ // Cells loaded from a file share one style object per xf. The object is only
55
+ // copied when the style is handed out (cell.style), so code that mutates it
56
+ // in place (cell.style.fill = ...) changes this cell alone.
57
+ get style() {
58
+ if (this._styleShared) {
59
+ this._style = copyStyle(this._style);
60
+ this._styleShared = false;
61
+ }
62
+ return this._style;
63
+ }
64
+
65
+ set style(value) {
66
+ this._style = value;
67
+ this._styleShared = false;
68
+ }
69
+
70
+ // use a style object that other cells may share (e.g. a cached xf style)
71
+ _setSharedStyle(style) {
72
+ this._style = style;
73
+ this._styleShared = true;
74
+ }
75
+
51
76
  get numFmt() {
52
- return this.style.numFmt;
77
+ return this._style.numFmt;
53
78
  }
54
79
 
55
80
  set numFmt(value) {
@@ -57,7 +82,7 @@ class Cell {
57
82
  }
58
83
 
59
84
  get font() {
60
- return this.style.font;
85
+ return this._style.font;
61
86
  }
62
87
 
63
88
  set font(value) {
@@ -65,7 +90,7 @@ class Cell {
65
90
  }
66
91
 
67
92
  get alignment() {
68
- return this.style.alignment;
93
+ return this._style.alignment;
69
94
  }
70
95
 
71
96
  set alignment(value) {
@@ -73,7 +98,7 @@ class Cell {
73
98
  }
74
99
 
75
100
  get border() {
76
- return this.style.border;
101
+ return this._style.border;
77
102
  }
78
103
 
79
104
  set border(value) {
@@ -81,7 +106,7 @@ class Cell {
81
106
  }
82
107
 
83
108
  get fill() {
84
- return this.style.fill;
109
+ return this._style.fill;
85
110
  }
86
111
 
87
112
  set fill(value) {
@@ -89,7 +114,7 @@ class Cell {
89
114
  }
90
115
 
91
116
  get protection() {
92
- return this.style.protection;
117
+ return this._style.protection;
93
118
  }
94
119
 
95
120
  set protection(value) {
@@ -99,7 +124,7 @@ class Cell {
99
124
  _setStyle(name, value) {
100
125
  // style objects can be shared (e.g. by every cell loaded with the same xf),
101
126
  // so never mutate one in place; give this cell a new object instead
102
- const oldStyle = this.style;
127
+ const oldStyle = this._style;
103
128
  const style = {...oldStyle, [name]: value};
104
129
  this.style = style;
105
130
 
@@ -112,12 +137,12 @@ class Cell {
112
137
  (Array.isArray(merges)
113
138
  ? merges.find(merge => merge.top === master.row && merge.left === master.col)
114
139
  : merges[master.address]);
115
- if (range && (master === this || master.style === oldStyle)) {
140
+ if (range && (master === this || master._style === oldStyle)) {
116
141
  for (let r = range.top; r <= range.bottom; r++) {
117
142
  const row = this.worksheet.findRow(r);
118
143
  for (let c = range.left; row && c <= range.right; c++) {
119
144
  const cell = row.findCell(c);
120
- if (cell && cell.style === oldStyle) {
145
+ if (cell && cell._style === oldStyle) {
121
146
  cell.style = style;
122
147
  }
123
148
  }
@@ -357,7 +382,7 @@ class Cell {
357
382
 
358
383
  get model() {
359
384
  const {model} = this._value;
360
- model.style = this.style;
385
+ model.style = this._style;
361
386
  if (this._comment) {
362
387
  model.comment = this._comment.model;
363
388
  }
@@ -378,7 +403,7 @@ class Cell {
378
403
  }
379
404
 
380
405
  if (value.style) {
381
- this.style = value.style;
406
+ this._setSharedStyle(value.style);
382
407
  } else {
383
408
  this.style = {};
384
409
  }
@@ -1,19 +1,101 @@
1
+ const colCache = require('../utils/col-cache');
2
+
3
+ function isRange(address) {
4
+ return typeof address === 'string' && address.includes(':');
5
+ }
6
+
1
7
  class DataValidations {
2
8
  constructor(model) {
3
9
  this.model = model || {};
10
+ // Range-keyed entries (e.g. 'A2:A1048576', kept intact when a file is loaded) are tracked
11
+ // separately so per-cell add/find stay O(1) when there are none (exceljs#1842).
12
+ this._ranges = new Map();
13
+ Object.keys(this.model).forEach(key => {
14
+ if (isRange(key)) {
15
+ this._addRange(key);
16
+ }
17
+ });
4
18
  }
5
19
 
6
20
  add(address, validation) {
21
+ if (isRange(address)) {
22
+ this._addRange(address);
23
+ } else {
24
+ this._splitRangeAt(address);
25
+ }
7
26
  return (this.model[address] = validation);
8
27
  }
9
28
 
10
29
  find(address) {
11
- return this.model[address];
30
+ const validation = this.model[address];
31
+ if (validation !== undefined || isRange(address) || !this._ranges.size) {
32
+ return validation;
33
+ }
34
+ // fall back to range-keyed entries that contain the address
35
+ const key = this._findRangeKey(address);
36
+ return key ? this.model[key] : undefined;
12
37
  }
13
38
 
14
39
  remove(address) {
40
+ if (isRange(address)) {
41
+ this._ranges.delete(address);
42
+ } else {
43
+ this._splitRangeAt(address);
44
+ }
15
45
  this.model[address] = undefined;
16
46
  }
47
+
48
+ _addRange(key) {
49
+ if (!this._ranges.has(key)) {
50
+ this._ranges.set(key, colCache.decodeEx(key));
51
+ }
52
+ }
53
+
54
+ _findRangeKey(address) {
55
+ if (!this._ranges.size) {
56
+ return undefined;
57
+ }
58
+ const {row, col} = colCache.decodeAddress(address);
59
+ for (const [key, {top, left, bottom, right}] of this._ranges) {
60
+ if (this.model[key] && row >= top && row <= bottom && col >= left && col <= right) {
61
+ return key;
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ // If a single cell lies inside a range-keyed entry, replace that entry with up to four
68
+ // rectangles covering the rest of the range, so the cell can get its own validation
69
+ // (or none) without overlapping sqrefs.
70
+ _splitRangeAt(address) {
71
+ const key = this._findRangeKey(address);
72
+ if (!key) {
73
+ return;
74
+ }
75
+ const validation = this.model[key];
76
+ const {row, col} = colCache.decodeAddress(address);
77
+ const {top, left, bottom, right} = this._ranges.get(key);
78
+ delete this.model[key];
79
+ this._ranges.delete(key);
80
+
81
+ const addRect = (t, l, b, r) => {
82
+ if (t > b || l > r) {
83
+ return;
84
+ }
85
+ const tl = colCache.encodeAddress(t, l);
86
+ if (t === b && l === r) {
87
+ this.model[tl] = validation;
88
+ } else {
89
+ const rangeKey = `${tl}:${colCache.encodeAddress(b, r)}`;
90
+ this.model[rangeKey] = validation;
91
+ this._addRange(rangeKey);
92
+ }
93
+ };
94
+ addRect(top, left, row - 1, right); // rows above
95
+ addRect(row + 1, left, bottom, right); // rows below
96
+ addRect(row, left, row, col - 1); // left of the cell
97
+ addRect(row, col + 1, row, right); // right of the cell
98
+ }
17
99
  }
18
100
 
19
101
  module.exports = DataValidations;
package/lib/doc/image.js CHANGED
@@ -56,4 +56,36 @@ class Image {
56
56
  }
57
57
  }
58
58
 
59
+ function isFiniteNumber(value) {
60
+ return typeof value === 'number' && Number.isFinite(value);
61
+ }
62
+
63
+ Image.validateRange = function validateRange(range) {
64
+ if (typeof range === 'string') {
65
+ let decoded;
66
+ try {
67
+ decoded = colCache.decode(range);
68
+ } catch (e) {
69
+ decoded = {};
70
+ }
71
+ const isRange = ['top', 'left', 'bottom', 'right'].every(key => isFiniteNumber(decoded[key]));
72
+ const isAddress = isFiniteNumber(decoded.row) && isFiniteNumber(decoded.col);
73
+ if (!isRange && !isAddress) {
74
+ throw new Error(`Invalid image range "${range}": expected a cell range such as "B2:D6"`);
75
+ }
76
+ return;
77
+ }
78
+ if (!range || typeof range !== 'object') {
79
+ throw new Error(
80
+ 'Invalid image range: expected a cell range string or an object with tl and br or ext'
81
+ );
82
+ }
83
+ if (!range.br && !range.ext) {
84
+ throw new Error('Invalid image range: an object range needs either br or ext');
85
+ }
86
+ if (range.ext && typeof range.ext !== 'object') {
87
+ throw new Error('Invalid image range: ext must be an object with width and height');
88
+ }
89
+ };
90
+
59
91
  module.exports = Image;
@@ -781,7 +781,15 @@ class Worksheet {
781
781
 
782
782
  // =========================================================================
783
783
  // Images
784
+ _validateImageId(imageId) {
785
+ if (imageId === undefined || imageId === null || !this._workbook.getImage(imageId)) {
786
+ throw new Error(`Image with id ${imageId} not found. Add it with workbook.addImage() first`);
787
+ }
788
+ }
789
+
784
790
  addImage(imageId, range) {
791
+ this._validateImageId(imageId);
792
+ Image.validateRange(range);
785
793
  const model = {
786
794
  type: 'image',
787
795
  imageId,
@@ -795,6 +803,7 @@ class Worksheet {
795
803
  }
796
804
 
797
805
  addBackgroundImage(imageId) {
806
+ this._validateImageId(imageId);
798
807
  const model = {
799
808
  type: 'background',
800
809
  imageId,
@@ -108,7 +108,11 @@ class WorkbookReader extends EventEmitter {
108
108
  if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
109
109
  match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
110
110
  sheetNo = match[1];
111
- if (this.sharedStrings && this.workbookRels) {
111
+ if (
112
+ this.sharedStrings &&
113
+ this.workbookRels &&
114
+ (this.options.styles !== 'cache' || this.stylesParsed)
115
+ ) {
112
116
  yield* this._parseWorksheet(iterateStream(entry), sheetNo);
113
117
  } else {
114
118
  // create temp file for each worksheet
@@ -316,6 +320,7 @@ class WorkbookReader extends EventEmitter {
316
320
  if (this.options.styles === 'cache') {
317
321
  this.styles = new StyleManager();
318
322
  await this.styles.parseStream(iterateStream(entry));
323
+ this.stylesParsed = true;
319
324
  }
320
325
  }
321
326
 
@@ -14,6 +14,7 @@ const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
14
14
  const AppXform = require('../../xlsx/xform/core/app-xform');
15
15
  const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
16
16
  const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
17
+ const DrawingXform = require('../../xlsx/xform/drawing/drawing-xform');
17
18
 
18
19
  const WorksheetWriter = require('./worksheet-writer');
19
20
 
@@ -45,6 +46,7 @@ class WorkbookWriter {
45
46
  this.zipOptions = options.zip;
46
47
 
47
48
  this.media = [];
49
+ this.drawings = [];
48
50
  this.commentRefs = [];
49
51
 
50
52
  this.zip = Archiver('zip', this.zipOptions);
@@ -99,6 +101,7 @@ class WorkbookWriter {
99
101
  await this.promise;
100
102
  await this.addMedia();
101
103
  await this._commitWorksheets();
104
+ await this.addDrawings();
102
105
  await Promise.all([
103
106
  this.addContentTypes(),
104
107
  this.addApp(),
@@ -219,6 +222,7 @@ class WorkbookWriter {
219
222
  sharedStrings: this.sharedStrings,
220
223
  commentRefs: this.commentRefs,
221
224
  media: this.media,
225
+ drawings: this.drawings,
222
226
  };
223
227
  const xform = new ContentTypesXform();
224
228
  const xml = xform.toXml(model);
@@ -249,6 +253,21 @@ class WorkbookWriter {
249
253
  );
250
254
  }
251
255
 
256
+ addDrawings() {
257
+ return new Promise(resolve => {
258
+ const drawingXform = new DrawingXform();
259
+ const relsXform = new RelationshipsXform();
260
+ this.drawings.forEach(drawing => {
261
+ drawingXform.prepare(drawing, {});
262
+ this.zip.append(drawingXform.toXml(drawing), {name: `xl/drawings/${drawing.name}.xml`});
263
+ this.zip.append(relsXform.toXml(drawing.rels), {
264
+ name: `xl/drawings/_rels/${drawing.name}.xml.rels`,
265
+ });
266
+ });
267
+ resolve();
268
+ });
269
+ }
270
+
252
271
  addApp() {
253
272
  return new Promise(resolve => {
254
273
  const model = {
@@ -283,7 +283,7 @@ class WorksheetReader extends EventEmitter {
283
283
  if (c.s) {
284
284
  const style = styles.getStyleModel(c.s);
285
285
  if (style) {
286
- cell.style = style;
286
+ cell._setSharedStyle(style);
287
287
  }
288
288
  }
289
289
 
@@ -13,6 +13,7 @@ const Column = require('../../doc/column');
13
13
  const SheetRelsWriter = require('./sheet-rels-writer');
14
14
  const SheetCommentsWriter = require('./sheet-comments-writer');
15
15
  const DataValidations = require('../../doc/data-validations');
16
+ const Image = require('../../doc/image');
16
17
 
17
18
  const xmlBuffer = new StringBuf();
18
19
 
@@ -246,6 +247,7 @@ class WorksheetWriter {
246
247
  this._writePageSetup();
247
248
  this._writeHeaderFooter();
248
249
  this._writeRowBreaks();
250
+ this._writeDrawing(); // must be after rowBreaks and before legacyDrawing
249
251
 
250
252
  // Legacy Data tag for comments
251
253
  this._writeLegacyData();
@@ -472,7 +474,34 @@ class WorksheetWriter {
472
474
 
473
475
  // =========================================================================
474
476
 
477
+ _validateImageId(imageId) {
478
+ if (imageId === undefined || imageId === null || !this._workbook.getImage(imageId)) {
479
+ throw new Error(`Image with id ${imageId} not found. Add it with workbook.addImage() first`);
480
+ }
481
+ }
482
+
483
+ addImage(imageId, range) {
484
+ if (this.committed) {
485
+ throw new Error('Cannot add an image to a committed worksheet');
486
+ }
487
+ this._validateImageId(imageId);
488
+ Image.validateRange(range);
489
+ // Anchors look up column widths and row heights to convert fractional positions.
490
+ // Only look at what already exists: getRow() would create (and later write) empty rows.
491
+ const lookup = {
492
+ properties: this.properties,
493
+ getColumn: c => this._columns && this._columns[c - 1],
494
+ getRow: r => this.findRow(r),
495
+ };
496
+ this._media.push(new Image(lookup, {type: 'image', imageId, range}));
497
+ }
498
+
499
+ getImages() {
500
+ return this._media.filter(m => m.type === 'image');
501
+ }
502
+
475
503
  addBackgroundImage(imageId) {
504
+ this._validateImageId(imageId);
476
505
  this._background = {
477
506
  imageId,
478
507
  };
@@ -696,6 +725,50 @@ class WorksheetWriter {
696
725
  }
697
726
  }
698
727
 
728
+ _writeDrawing() {
729
+ const images = this.getImages();
730
+ if (!images.length) {
731
+ return;
732
+ }
733
+ const name = `drawing${this.id}`;
734
+ const rId = this._sheetRelsWriter.addRelationship({
735
+ Target: `../drawings/${name}.xml`,
736
+ Type: RelType.Drawing,
737
+ });
738
+ const drawing = {rId, name, anchors: [], rels: []};
739
+ const nextRid = () => `rId${drawing.rels.length + 1}`;
740
+ const imageRIds = {};
741
+ images.forEach(image => {
742
+ const {imageId, range, hyperlinks} = image.model;
743
+ let imageRId = imageRIds[imageId];
744
+ if (!imageRId) {
745
+ imageRId = imageRIds[imageId] = nextRid();
746
+ drawing.rels.push({
747
+ Id: imageRId,
748
+ Type: RelType.Image,
749
+ Target: `../media/${this._workbook.getImage(imageId).name}`,
750
+ });
751
+ }
752
+ const anchor = {picture: {rId: imageRId}, range};
753
+ if (hyperlinks && hyperlinks.hyperlink) {
754
+ const hyperlinkRId = nextRid();
755
+ anchor.picture.hyperlinks = {tooltip: hyperlinks.tooltip, rId: hyperlinkRId};
756
+ drawing.rels.push({
757
+ Id: hyperlinkRId,
758
+ Type: RelType.Hyperlink,
759
+ Target: hyperlinks.hyperlink,
760
+ TargetMode: 'External',
761
+ });
762
+ }
763
+ drawing.anchors.push(anchor);
764
+ });
765
+ this._workbook.drawings.push(drawing);
766
+
767
+ xmlBuffer.reset();
768
+ xmlBuffer.addText(`<drawing r:id="${rId}"/>`);
769
+ this.stream.write(xmlBuffer);
770
+ }
771
+
699
772
  _writeLegacyData() {
700
773
  if (this.hasComments) {
701
774
  xmlBuffer.reset();
@@ -12,6 +12,7 @@ module.exports = {
12
12
  CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
13
13
  ExtenderProperties: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
14
14
  Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
15
+ Drawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
15
16
  VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
16
17
  Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
17
18
  PivotCacheDefinition: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
@@ -22,16 +22,55 @@ class AutoFilterXform extends BaseXform {
22
22
  const firstAddress = getAddress(model.from);
23
23
  const secondAddress = getAddress(model.to);
24
24
  if (firstAddress && secondAddress) {
25
- xmlStream.leafNode('autoFilter', {ref: `${firstAddress}:${secondAddress}`});
25
+ const ref = `${firstAddress}:${secondAddress}`;
26
+ if (model.exclude && model.exclude.length) {
27
+ // hide the filter button on the excluded columns (0 = leftmost column of the range)
28
+ xmlStream.openNode('autoFilter', {ref});
29
+ model.exclude.forEach(colId => {
30
+ xmlStream.leafNode('filterColumn', {colId, hiddenButton: '1'});
31
+ });
32
+ xmlStream.closeNode();
33
+ } else {
34
+ xmlStream.leafNode('autoFilter', {ref});
35
+ }
26
36
  }
27
37
  }
28
38
  }
29
39
  }
30
40
 
31
41
  parseOpen(node) {
32
- if (node.name === 'autoFilter') {
33
- this.model = node.attributes.ref;
42
+ switch (node.name) {
43
+ case 'autoFilter':
44
+ this.ref = node.attributes.ref;
45
+ this.exclude = [];
46
+ this.model = this.ref;
47
+ return true;
48
+ case 'filterColumn': {
49
+ const {colId, hiddenButton, showButton} = node.attributes;
50
+ const hidden =
51
+ hiddenButton === '1' ||
52
+ hiddenButton === 'true' ||
53
+ showButton === '0' ||
54
+ showButton === 'false';
55
+ if (hidden && colId !== undefined) {
56
+ this.exclude.push(parseInt(colId, 10));
57
+ }
58
+ return true;
59
+ }
60
+ default:
61
+ return true;
62
+ }
63
+ }
64
+
65
+ parseClose(name) {
66
+ if (name === 'autoFilter') {
67
+ if (this.exclude.length && this.ref) {
68
+ const [from, to] = this.ref.split(':');
69
+ this.model = {from, to: to || from, exclude: this.exclude};
70
+ }
71
+ return false;
34
72
  }
73
+ return true;
35
74
  }
36
75
  }
37
76
 
@@ -4,6 +4,9 @@ const colCache = require('../../../utils/col-cache');
4
4
  const BaseXform = require('../base-xform');
5
5
  const Range = require('../../../doc/range');
6
6
 
7
+ // sqref ranges with more cells than this are kept as a single range-keyed model entry
8
+ const MAX_EXPANDED_RANGE_CELLS = 1000;
9
+
7
10
  function assign(definedName, attributes, name, defaultValue) {
8
11
  const value = attributes[name];
9
12
  if (value !== undefined) {
@@ -215,12 +218,20 @@ class DataValidationsXform extends BaseXform {
215
218
  }
216
219
  // The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
217
220
  const list = this._address.split(/\s+/g) || [];
221
+ // Large ranges (e.g. a whole column 'A2:A1048576') are kept as one range-keyed entry
222
+ // instead of being expanded into one entry per cell, which could take minutes and
223
+ // gigabytes (exceljs#1842). DataValidations.find() resolves them. Small ranges are
224
+ // still expanded so the model keeps its per-cell shape and lookups stay O(1).
218
225
  list.forEach(addr => {
219
226
  if (addr.includes(':')) {
220
227
  const range = new Range(addr);
221
- range.forEachAddress(address => {
222
- this.model[address] = this._dataValidation;
223
- });
228
+ if (range.count > MAX_EXPANDED_RANGE_CELLS) {
229
+ this.model[range.range] = this._dataValidation;
230
+ } else {
231
+ range.forEachAddress(address => {
232
+ this.model[address] = this._dataValidation;
233
+ });
234
+ }
224
235
  } else {
225
236
  this.model[addr] = this._dataValidation;
226
237
  }