diff --git a/README.md b/README.md
index 5ba580b..6b51bb8 100644
--- a/README.md
+++ b/README.md
@@ -216,6 +216,7 @@ To be clear, all contributions added to this library will be included in the lib
Conditional Formatting
Outline Levels
Images
+ Shape
Sheet Protection
File I/O
@@ -2209,6 +2210,31 @@ worksheet.addImage(imageId2, {
});
```
+## Shape
+
+### Add shape to worksheet[⬆](#contents)
+
+```javascript
+worksheet.addShape({
+ type: 'roundRect',
+ rotation: 0,
+ fill: { type: 'solid', color: { rgb: '4499FF' } },
+ outline: { weight: 2, color: { rgb: '446699' }, dash: 'sysDash' },
+ textBody: {
+ vertAlign: 'ctr',
+ paragraphs: [
+ { alignment: 'l', runs: ["Lorem ipsum dolor sit amet, consectetur adipiscing elit."] },
+ { alignment: 'r', runs: [
+ { text: "Nulla eget odio sed libero ultrices vehicula.", font: { bold: true, color: { rgb: 'FF0000' } } },
+ ] },
+ ],
+ },
+}, 'B2:H8', {
+ hyperlink: 'https://www.example.com',
+ tooltip: 'Example Link',
+});
+```
+
## Sheet Protection[⬆](#contents)
Worksheets can be protected from modification by adding a password.
diff --git a/README_zh.md b/README_zh.md
index b2461ad..78b674b 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -176,6 +176,7 @@ ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#A1:B1' };
- 条件格式化
- 大纲级别
- 图片
+ - 形状Shape
- 工作表保护
- 文件 I/O
@@ -2106,6 +2107,31 @@ worksheet.addImage(imageId2, {
});
```
+## 形状(Shape)
+
+### 新增形状(Shape)到工作表[⬆](#contents)
+
+```javascript
+worksheet.addShape({
+ type: 'roundRect',
+ rotation: 0,
+ fill: { type: 'solid', color: { rgb: '4499FF' } },
+ outline: { weight: 2, color: { rgb: '446699' }, dash: 'sysDash' },
+ textBody: {
+ vertAlign: 'ctr',
+ paragraphs: [
+ { alignment: 'l', runs: ["Lorem ipsum dolor sit amet, consectetur adipiscing elit."] },
+ { alignment: 'r', runs: [
+ { text: "Nulla eget odio sed libero ultrices vehicula.", font: { bold: true, color: { rgb: 'FF0000' } } },
+ ] },
+ ],
+ },
+}, 'B2:H8', {
+ hyperlink: 'https://www.example.com',
+ tooltip: 'Example Link',
+});
+```
+
## 工作表保护[⬆](#目录)
可以通过添加密码来保护工作表免受修改。
diff --git a/index.d.ts b/index.d.ts
index 5434cf1..afed5cf 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1004,21 +1004,79 @@ export class Anchor implements IAnchor {
constructor(model?: IAnchor | object);
}
-export interface ImageRange {
+export interface DrawingRange {
tl: Anchor;
br: Anchor;
}
-export interface ImagePosition {
+export interface DrawingPosition {
tl: { col: number; row: number };
ext: { width: number; height: number };
}
-export interface ImageHyperlinkValue {
+export interface DrawingHyperlinkValue {
hyperlink: string;
tooltip?: string;
}
+export interface ShapeProps {
+ /**
+ * Defined as DocumentFormat.OpenXml.Drawing.ShapeTypeValues in Open API Spec.
+ * See https://learn.microsoft.com/ja-jp/dotnet/api/documentformat.openxml.drawing.shapetypevalues
+ */
+ type: string;
+ rotation?: number;
+ horizontalFlip?: boolean;
+ verticalFlip?: boolean;
+ fill?: ShapeFill;
+ outline?: ShapeOutline;
+ textBody?: ShapeTextBody;
+}
+
+export type ShapeFill = {
+ type: 'solid',
+ color: { theme?: string, rgb?: string }
+}
+
+export interface ShapeArrowEnd {
+ type?: 'triangle' | 'arrow' | 'stealth' | 'diamond' | 'oval';
+ length?: 'lg' | 'med' | 'sm';
+ width?: 'lg' | 'med' | 'sm';
+}
+
+export type ShapeOutline = {
+ weight?: number,
+ color?: { theme?: string, rgb?: string },
+ dash?: 'solid' | 'sysDot' | 'sysDash' | 'dash' | 'dashDot' | 'lgDash' | 'lgDashDot' | 'lgDashDotDot',
+ arrow?: {
+ head?: ShapeArrowEnd,
+ tail?: ShapeArrowEnd
+ }
+}
+
+export type ShapeTextBody = {
+ paragraphs: ShapeParagraph[],
+ vertAlign?: 't' | 'ctr' | 'b',
+}
+
+export type ShapeParagraph = {
+ runs: ShapeRun[],
+ alignment?: 'l' | 'ctr' | 'r',
+}
+
+export type ShapeRun = {
+ text: string,
+ font?: Partial,
+}
+
+export type ShapeRunFont = {
+ size: number,
+ color: { theme?: string, rgb?: string },
+ bold: boolean,
+ italic: boolean,
+ underline: 'sng' | 'dbl' | 'none',
+}
+
export interface Range extends Location {
sheetName: string;
@@ -1458,14 +1516,21 @@ export interface Worksheet {
* Using the image id from `Workbook.addImage`,
* embed an image within the worksheet to cover a range
*/
- addImage(imageId: number, range: string | { editAs?: string; } & ImageRange & { hyperlinks?: ImageHyperlinkValue } | { editAs?: string; } & ImagePosition & { hyperlinks?: ImageHyperlinkValue }): void;
+ addImage(imageId: number, range: string | { editAs?: string; } & DrawingRange & { hyperlinks?: DrawingHyperlinkValue } | { editAs?: string; } & DrawingPosition & { hyperlinks?: DrawingHyperlinkValue }): void;
getImages(): Array<{
type: 'image',
- imageId: string;
- range: ImageRange;
+ imageId: string,
+ range: DrawingRange,
}>;
+ addShape(props: ShapeProps, range: string | { editAs?: string; } & DrawingRange | { editAs?: string; } & DrawingPosition, hyperlinks?: DrawingHyperlinkValue ): void;
+
+ getShapes(): Array<{
+ props: ShapeProps,
+ range: DrawingRange,
+ }>
+
commit(): void;
model: WorksheetModel;
diff --git a/lib/doc/drawing-range.js b/lib/doc/drawing-range.js
new file mode 100644
index 0000000..29395ae
--- /dev/null
+++ b/lib/doc/drawing-range.js
@@ -0,0 +1,22 @@
+const colCache = require('../utils/col-cache');
+const Anchor = require('./anchor');
+
+module.exports = {
+ parseRange: (range, hyperlinks, worksheet) => {
+ if (typeof range === 'string') {
+ const decoded = colCache.decode(range);
+ return {
+ tl: new Anchor(worksheet, {col: decoded.left, row: decoded.top}, -1),
+ br: new Anchor(worksheet, {col: decoded.right, row: decoded.bottom}, 0),
+ editAs: 'oneCell',
+ };
+ }
+ return {
+ tl: new Anchor(worksheet, range.tl, 0),
+ br: range.br && new Anchor(worksheet, range.br, 0),
+ ext: range.ext,
+ editAs: range.editAs,
+ hyperlinks: hyperlinks || range.hyperlinks,
+ };
+ },
+};
\ No newline at end of file
diff --git a/lib/doc/image.js b/lib/doc/image.js
index beefae8..a7b24e7 100644
--- a/lib/doc/image.js
+++ b/lib/doc/image.js
@@ -1,5 +1,4 @@
-const colCache = require('../utils/col-cache');
-const Anchor = require('./anchor');
+const {parseRange} = require('./drawing-range');
class Image {
constructor(worksheet, model) {
@@ -36,24 +35,9 @@ class Image {
this.imageId = imageId;
if (type === 'image') {
- if (typeof range === 'string') {
- const decoded = colCache.decode(range);
- this.range = {
- tl: new Anchor(this.worksheet, {col: decoded.left, row: decoded.top}, -1),
- br: new Anchor(this.worksheet, {col: decoded.right, row: decoded.bottom}, 0),
- editAs: 'oneCell',
- };
- } else {
- this.range = {
- tl: new Anchor(this.worksheet, range.tl, 0),
- br: range.br && new Anchor(this.worksheet, range.br, 0),
- ext: range.ext,
- editAs: range.editAs,
- hyperlinks: hyperlinks || range.hyperlinks,
- };
- }
+ this.range = parseRange(range, hyperlinks, this.worksheet);
}
}
}
-module.exports = Image;
+module.exports = Image;
\ No newline at end of file
diff --git a/lib/doc/shape.js b/lib/doc/shape.js
new file mode 100644
index 0000000..21829e7
--- /dev/null
+++ b/lib/doc/shape.js
@@ -0,0 +1,110 @@
+const {parseRange} = require('./drawing-range');
+
+class Shape {
+ constructor(worksheet, model) {
+ this.worksheet = worksheet;
+ this.model = model;
+ }
+
+ get model() {
+ return {
+ props: {
+ type: this.props.type,
+ rotation: this.props.rotation,
+ horizontalFlip: this.props.horizontalFlip,
+ verticalFlip: this.props.verticalFlip,
+ fill: this.props.fill,
+ outline: this.props.outline,
+ textBody: this.props.textBody,
+ },
+ range: {
+ tl: this.range.tl.model,
+ br: this.range.br && this.range.br.model,
+ ext: this.range.ext,
+ editAs: this.range.editAs,
+ },
+ hyperlinks: this.hyperlinks,
+ };
+ }
+
+ set model({props, range, hyperlinks}) {
+ this.props = {type: props.type};
+ if (props.rotation) {
+ this.props.rotation = props.rotation;
+ }
+ if (props.horizontalFlip) {
+ this.props.horizontalFlip = props.horizontalFlip;
+ }
+ if (props.verticalFlip) {
+ this.props.verticalFlip = props.verticalFlip;
+ }
+ if (props.fill) {
+ this.props.fill = props.fill;
+ }
+ if (props.outline) {
+ this.props.outline = props.outline;
+ }
+ if (props.textBody) {
+ this.props.textBody = parseAsTextBody(props.textBody);
+ }
+ this.range = parseRange(range, undefined, this.worksheet);
+ this.hyperlinks = hyperlinks;
+ }
+}
+
+function parseAsTextBody(input) {
+ if (typeof input === 'string') {
+ return {
+ paragraphs: [parseAsParagraph(input)],
+ };
+ }
+ if (Array.isArray(input)) {
+ return {
+ paragraphs: input.map(parseAsParagraph),
+ };
+ }
+ const model = {
+ paragraphs: input.paragraphs.map(parseAsParagraph),
+ };
+ if (input.vertAlign) {
+ model.vertAlign = input.vertAlign;
+ }
+ return model;
+}
+
+function parseAsParagraph(input) {
+ if (typeof input === 'string') {
+ return {
+ runs: [parseAsRun(input)],
+ };
+ }
+ if (Array.isArray(input)) {
+ return {
+ runs: input.map(parseAsRun),
+ };
+ }
+ const model = {
+ runs: input.runs.map(parseAsRun),
+ };
+ if (input.alignment) {
+ model.alignment = input.alignment;
+ }
+ return model;
+}
+
+function parseAsRun(input) {
+ if (typeof input === 'string') {
+ return {
+ text: input,
+ };
+ }
+ const model = {
+ text: input.text,
+ };
+ if (input.font) {
+ model.font = input.font;
+ }
+ return model;
+}
+
+module.exports = Shape;
\ No newline at end of file
diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js
index a5a8892..1839f45 100644
--- a/lib/doc/worksheet.js
+++ b/lib/doc/worksheet.js
@@ -6,6 +6,7 @@ const Row = require('./row');
const Column = require('./column');
const Enums = require('./enums');
const Image = require('./image');
+const Shape = require('./shape');
const Table = require('./table');
const DataValidations = require('./data-validations');
const Encryptor = require('../utils/encryptor');
@@ -120,6 +121,9 @@ class Worksheet {
// for images, etc
this._media = [];
+ // for shapes
+ this.shapes = [];
+
// worksheet protection
this.sheetProtection = null;
@@ -750,6 +754,16 @@ class Worksheet {
return image && image.imageId;
}
+ // =========================================================================
+ // Shapes
+ addShape(props, range, hyperlinks) {
+ this.shapes.push(new Shape(this, {props, range, hyperlinks}));
+ }
+
+ getShapes() {
+ return this.shapes;
+ }
+
// =========================================================================
// Worksheet Protection
protect(password, options) {
@@ -855,6 +869,7 @@ class Worksheet {
views: this.views,
autoFilter: this.autoFilter,
media: this._media.map(medium => medium.model),
+ shapes: this.shapes,
sheetProtection: this.sheetProtection,
tables: Object.values(this.tables).map(table => table.model),
conditionalFormattings: this.conditionalFormattings,
@@ -916,6 +931,7 @@ class Worksheet {
this.views = value.views;
this.autoFilter = value.autoFilter;
this._media = value.media.map(medium => new Image(this, medium));
+ this.shapes = value.shapes;
this.sheetProtection = value.sheetProtection;
this.tables = value.tables.reduce((tables, table) => {
const t = new Table();
diff --git a/lib/xlsx/xform/drawing/c-nv-pr-xform.js b/lib/xlsx/xform/drawing/c-nv-pr-xform.js
index 579affe..a1ee2fa 100644
--- a/lib/xlsx/xform/drawing/c-nv-pr-xform.js
+++ b/lib/xlsx/xform/drawing/c-nv-pr-xform.js
@@ -3,9 +3,11 @@ const HlickClickXform = require('./hlink-click-xform');
const ExtLstXform = require('./ext-lst-xform');
class CNvPrXform extends BaseXform {
- constructor() {
+ constructor(isPicture) {
super();
+ this.isPicture = isPicture;
+
this.map = {
'a:hlinkClick': new HlickClickXform(),
'a:extLst': new ExtLstXform(),
@@ -19,7 +21,7 @@ class CNvPrXform extends BaseXform {
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
id: model.index,
- name: `Picture ${model.index}`,
+ name: `${this.isPicture ? 'Picture' : 'Shape'} ${model.index}`,
});
this.map['a:hlinkClick'].render(xmlStream, model);
this.map['a:extLst'].render(xmlStream, model);
diff --git a/lib/xlsx/xform/drawing/one-cell-anchor-xform.js b/lib/xlsx/xform/drawing/one-cell-anchor-xform.js
index dab78db..cca32c1 100644
--- a/lib/xlsx/xform/drawing/one-cell-anchor-xform.js
+++ b/lib/xlsx/xform/drawing/one-cell-anchor-xform.js
@@ -3,7 +3,8 @@ const StaticXform = require('../static-xform');
const CellPositionXform = require('./cell-position-xform');
const ExtXform = require('./ext-xform');
-const PicXform = require('./pic-xform');
+const PicXform = require('./picture/pic-xform');
+const SpXform = require('./shape/sp-xform');
class OneCellAnchorXform extends BaseCellAnchorXform {
constructor() {
@@ -13,6 +14,7 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
'xdr:from': new CellPositionXform({tag: 'xdr:from'}),
'xdr:ext': new ExtXform({tag: 'xdr:ext'}),
'xdr:pic': new PicXform(),
+ 'xdr:sp': new SpXform(),
'xdr:clientData': new StaticXform({tag: 'xdr:clientData'}),
};
}
@@ -22,7 +24,12 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
}
prepare(model, options) {
- this.map['xdr:pic'].prepare(model.picture, options);
+ if (model.picture) {
+ this.map['xdr:pic'].prepare(model.picture, options);
+ }
+ if (model.shape) {
+ this.map['xdr:sp'].prepare(model.shape, options);
+ }
}
render(xmlStream, model) {
@@ -30,7 +37,12 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
this.map['xdr:from'].render(xmlStream, model.range.tl);
this.map['xdr:ext'].render(xmlStream, model.range.ext);
- this.map['xdr:pic'].render(xmlStream, model.picture);
+ if (model.picture) {
+ this.map['xdr:pic'].render(xmlStream, model.picture);
+ }
+ if (model.shape) {
+ this.map['xdr:sp'].render(xmlStream, model.shape);
+ }
this.map['xdr:clientData'].render(xmlStream, {});
xmlStream.closeNode();
@@ -48,6 +60,7 @@ class OneCellAnchorXform extends BaseCellAnchorXform {
this.model.range.tl = this.map['xdr:from'].model;
this.model.range.ext = this.map['xdr:ext'].model;
this.model.picture = this.map['xdr:pic'].model;
+ this.model.shape = this.map['xdr:sp'].model;
return false;
default:
// could be some unrecognised tags
diff --git a/lib/xlsx/xform/drawing/c-nv-pic-pr-xform.js b/lib/xlsx/xform/drawing/picture/c-nv-pic-pr-xform.js
similarity index 92%
rename from lib/xlsx/xform/drawing/c-nv-pic-pr-xform.js
rename to lib/xlsx/xform/drawing/picture/c-nv-pic-pr-xform.js
index 536c9aa..844db82 100644
--- a/lib/xlsx/xform/drawing/c-nv-pic-pr-xform.js
+++ b/lib/xlsx/xform/drawing/picture/c-nv-pic-pr-xform.js
@@ -1,4 +1,4 @@
-const BaseXform = require('../base-xform');
+const BaseXform = require('../../base-xform');
class CNvPicPrXform extends BaseXform {
get tag() {
diff --git a/lib/xlsx/xform/drawing/nv-pic-pr-xform.js b/lib/xlsx/xform/drawing/picture/nv-pic-pr-xform.js
similarity index 89%
rename from lib/xlsx/xform/drawing/nv-pic-pr-xform.js
rename to lib/xlsx/xform/drawing/picture/nv-pic-pr-xform.js
index 7f80d61..78f89c9 100644
--- a/lib/xlsx/xform/drawing/nv-pic-pr-xform.js
+++ b/lib/xlsx/xform/drawing/picture/nv-pic-pr-xform.js
@@ -1,5 +1,5 @@
-const BaseXform = require('../base-xform');
-const CNvPrXform = require('./c-nv-pr-xform');
+const BaseXform = require('../../base-xform');
+const CNvPrXform = require('../c-nv-pr-xform');
const CNvPicPrXform = require('./c-nv-pic-pr-xform');
class NvPicPrXform extends BaseXform {
@@ -7,7 +7,7 @@ class NvPicPrXform extends BaseXform {
super();
this.map = {
- 'xdr:cNvPr': new CNvPrXform(),
+ 'xdr:cNvPr': new CNvPrXform(true),
'xdr:cNvPicPr': new CNvPicPrXform(),
};
}
diff --git a/lib/xlsx/xform/drawing/pic-xform.js b/lib/xlsx/xform/drawing/picture/pic-xform.js
similarity index 88%
rename from lib/xlsx/xform/drawing/pic-xform.js
rename to lib/xlsx/xform/drawing/picture/pic-xform.js
index 0833c7a..2ab69a4 100644
--- a/lib/xlsx/xform/drawing/pic-xform.js
+++ b/lib/xlsx/xform/drawing/picture/pic-xform.js
@@ -1,10 +1,10 @@
-const BaseXform = require('../base-xform');
-const StaticXform = require('../static-xform');
+const BaseXform = require('../../base-xform');
+const StaticXform = require('../../static-xform');
-const BlipFillXform = require('./blip-fill-xform');
+const BlipFillXform = require('../blip-fill-xform');
const NvPicPrXform = require('./nv-pic-pr-xform');
-const spPrJSON = require('./sp-pr');
+const spPrJSON = require('../sp-pr');
class PicXform extends BaseXform {
constructor() {
diff --git a/lib/xlsx/xform/drawing/shape/ln-xform.js b/lib/xlsx/xform/drawing/shape/ln-xform.js
new file mode 100644
index 0000000..bd6b810
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/ln-xform.js
@@ -0,0 +1,111 @@
+const BaseXform = require('../../base-xform');
+const SolidFillXform = require('./solid-fill-xform');
+
+// DocumentFormat.OpenXml.Drawing.Outline
+class LnXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:solidFill': new SolidFillXform(),
+ };
+ }
+
+ get tag() {
+ return 'a:ln';
+ }
+
+ render(xmlStream, outline) {
+ xmlStream.openNode(this.tag);
+ if (outline.weight) {
+ xmlStream.addAttribute('w', outline.weight * 12700);
+ }
+ if (outline.color) {
+ this.map['a:solidFill'].render(xmlStream, outline.color);
+ }
+ if (outline.dash) {
+ xmlStream.leafNode('a:prstDash', {val: outline.dash});
+ }
+ if (outline.arrow) {
+ if (outline.arrow.head) {
+ xmlStream.leafNode('a:headEnd', {
+ type: outline.arrow.head.type,
+ w: outline.arrow.head.width,
+ len: outline.arrow.head.length,
+ });
+ }
+ if (outline.arrow.tail) {
+ xmlStream.leafNode('a:tailEnd', {
+ type: outline.arrow.tail.type,
+ w: outline.arrow.tail.width,
+ len: outline.arrow.tail.length,
+ });
+ }
+ }
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {};
+ if (node.attributes.w) {
+ this.model.weight = parseInt(node.attributes.w, 10) / 12700;
+ }
+ break;
+ case 'a:prstDash':
+ this.model.dash = node.attributes.val;
+ break;
+ case 'a:headEnd':
+ this.model.arrow = this.model.arrow || {};
+ this.model.arrow.head = {
+ type: node.attributes.type,
+ width: node.attributes.w,
+ length: node.attributes.len,
+ };
+ break;
+ case 'a:tailEnd':
+ this.model.arrow = this.model.arrow || {};
+ this.model.arrow.tail = {
+ type: node.attributes.type,
+ width: node.attributes.w,
+ length: node.attributes.len,
+ };
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ if (this.map['a:solidFill'].model) {
+ this.model.color = this.map['a:solidFill'].model;
+ }
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = LnXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/nv-sp-pr-xform.js b/lib/xlsx/xform/drawing/shape/nv-sp-pr-xform.js
new file mode 100644
index 0000000..4be98db
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/nv-sp-pr-xform.js
@@ -0,0 +1,63 @@
+const BaseXform = require('../../base-xform');
+const CNvPrXform = require('../c-nv-pr-xform');
+
+// DocumentFormat.OpenXml.Drawing.Spreadsheet.NonVisualShapeProperties
+class NvSpPrXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'xdr:cNvPr': new CNvPrXform(false),
+ };
+ }
+
+ get tag() {
+ return 'xdr:nvSpPr';
+ }
+
+ render(xmlStream, shape) {
+ xmlStream.openNode(this.tag);
+ this.map['xdr:cNvPr'].render(xmlStream, shape);
+ xmlStream.leafNode('xdr:cNvSpPr');
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ this.model = this.map['xdr:cNvPr'].model;
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = NvSpPrXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/p-xform.js b/lib/xlsx/xform/drawing/shape/p-xform.js
new file mode 100644
index 0000000..511c3fd
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/p-xform.js
@@ -0,0 +1,81 @@
+const BaseXform = require('../../base-xform');
+const RunXform = require('./r-xform');
+
+// DocumentFormat.OpenXml.Drawing.Paragraph
+class ParagraphXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:r': new RunXform(),
+ };
+ }
+
+ get tag() {
+ return 'a:p';
+ }
+
+ render(xmlStream, paragraph) {
+ xmlStream.openNode('a:p');
+ xmlStream.openNode('a:pPr');
+ if (paragraph.alignment) {
+ xmlStream.addAttribute('algn', paragraph.alignment);
+ }
+ xmlStream.closeNode();
+ paragraph.runs.forEach(r => {
+ this.map['a:r'].render(xmlStream, r);
+ });
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {runs: []};
+ break;
+ case 'a:pPr':
+ if (node.attributes.algn) {
+ this.model.alignment = node.attributes.algn;
+ }
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText(text) {
+ if (this.parser) {
+ this.parser.parseText(text);
+ }
+ }
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ if (name === 'a:r') {
+ this.model.runs.push(this.parser.model);
+ }
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = ParagraphXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/prst-geom-xform.js b/lib/xlsx/xform/drawing/shape/prst-geom-xform.js
new file mode 100644
index 0000000..940a440
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/prst-geom-xform.js
@@ -0,0 +1,60 @@
+const BaseXform = require('../../base-xform');
+
+// DocumentFormat.OpenXml.Drawing.PresetGeometry
+class PrstGeomXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {};
+ }
+
+ get tag() {
+ return 'a:prstGeom';
+ }
+
+ render(xmlStream, model) {
+ xmlStream.openNode(this.tag, {prst: model.type});
+ xmlStream.leafNode('a:avLst', {});
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {type: node.attributes.prst};
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ // TOOD avLst
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = PrstGeomXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/r-xform.js b/lib/xlsx/xform/drawing/shape/r-xform.js
new file mode 100644
index 0000000..4edbe84
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/r-xform.js
@@ -0,0 +1,103 @@
+const BaseXform = require('../../base-xform');
+const SolidFillXform = require('./solid-fill-xform');
+
+// DocumentFormat.OpenXml.Drawing.Run
+class RunXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:solidFill': new SolidFillXform(),
+ };
+ }
+
+ get tag() {
+ return 'a:r';
+ }
+
+ render(xmlStream, run) {
+ xmlStream.openNode(this.tag);
+ xmlStream.openNode('a:rPr');
+ if (run.font) {
+ xmlStream.addAttributes({
+ sz: run.font.size ? run.font.size * 100 : undefined,
+ b: run.font.bold ? 1 : undefined,
+ i: run.font.italic ? 1 : undefined,
+ u: run.font.underline || undefined,
+ });
+ }
+ if (run.font && run.font.color) {
+ this.map['a:solidFill'].render(xmlStream, run.font.color);
+ }
+ xmlStream.closeNode();
+ xmlStream.leafNode('a:t', undefined, run.text);
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {text: '', font: {}};
+ this.parsingText = false;
+ break;
+ case 'a:rPr':
+ if (node.attributes.sz) {
+ this.model.font.size = parseInt(node.attributes.sz, 10) / 100;
+ }
+ if (node.attributes.b) {
+ this.model.font.bold = node.attributes.b === '1';
+ }
+ if (node.attributes.i) {
+ this.model.font.italic = node.attributes.i === '1';
+ }
+ if (node.attributes.u) {
+ this.model.font.underline = node.attributes.u;
+ }
+ break;
+ case 'a:t':
+ this.parsingText = true;
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText(text) {
+ if (this.parsingText) {
+ this.model.text = text.replace(/_x([0-9A-F]{4})_/g, ($0, $1) => String.fromCharCode(parseInt($1, 16)));
+ }
+ }
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ if (this.map['a:solidFill'].model) {
+ this.model.font.color = this.map['a:solidFill'].model;
+ }
+ return false;
+ case 'a:t':
+ this.parsingText = false;
+ return true;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = RunXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/solid-fill-xform.js b/lib/xlsx/xform/drawing/shape/solid-fill-xform.js
new file mode 100644
index 0000000..8fcb376
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/solid-fill-xform.js
@@ -0,0 +1,71 @@
+const BaseXform = require('../../base-xform');
+
+// DocumentFormat.OpenXml.Drawing.SolidFill
+class SolidFillXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {};
+ }
+
+ get tag() {
+ return 'a:solidFill';
+ }
+
+ render(xmlStream, color) {
+ xmlStream.openNode(this.tag);
+ if (color.theme) {
+ xmlStream.leafNode('a:schemeClr', {val: color.theme});
+ } else if (color.rgb) {
+ xmlStream.leafNode('a:srgbClr', {val: color.rgb});
+ }
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.reset();
+ this.model = {};
+ break;
+ case 'a:schemeClr':
+ this.model.theme = node.attributes.val;
+ break;
+ case 'a:srgbClr':
+ this.model.rgb = node.attributes.val;
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = SolidFillXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/sp-pr-xform.js b/lib/xlsx/xform/drawing/shape/sp-pr-xform.js
new file mode 100644
index 0000000..d2639de
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/sp-pr-xform.js
@@ -0,0 +1,96 @@
+const BaseXform = require('../../base-xform');
+const XfrmXform = require('../xfrm-xform');
+const PrstGeomXform = require('./prst-geom-xform');
+const SolidFillXform = require('./solid-fill-xform');
+const LnXform = require('./ln-xform');
+
+// DocumentFormat.OpenXml.Drawing.Spreadsheet.ShapeProperties
+class SpPrXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:xfrm': new XfrmXform(),
+ 'a:prstGeom': new PrstGeomXform(),
+ 'a:solidFill': new SolidFillXform(),
+ 'a:ln': new LnXform(),
+ };
+ }
+
+ get tag() {
+ return 'xdr:spPr';
+ }
+
+ render(xmlStream, shape) {
+ xmlStream.openNode(this.tag);
+ this.map['a:xfrm'].render(xmlStream, shape);
+ this.map['a:prstGeom'].render(xmlStream, shape);
+ if (shape.fill && shape.fill.type === 'solid') {
+ this.map['a:solidFill'].render(xmlStream, shape.fill.color);
+ } else {
+ xmlStream.leafNode('a:noFill');
+ }
+ if (shape.outline) {
+ this.map['a:ln'].render(xmlStream, shape.outline);
+ }
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {};
+ this.noFill = false;
+ break;
+ case 'a:noFill':
+ this.noFill = true;
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ if (this.map['a:prstGeom'].model) {
+ this.model.type = this.map['a:prstGeom'].model.type;
+ }
+ if (this.map['a:solidFill'].model) {
+ this.model.fill = {
+ type: 'solid',
+ color: this.map['a:solidFill'].model,
+ };
+ }
+ if (this.map['a:ln'].model) {
+ this.model.outline = this.map['a:ln'].model;
+ }
+ if (this.map['a:xfrm'].model) {
+ this.mergeModel(this.map['a:xfrm'].model);
+ }
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = SpPrXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/sp-xform.js b/lib/xlsx/xform/drawing/shape/sp-xform.js
new file mode 100644
index 0000000..bc9b674
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/sp-xform.js
@@ -0,0 +1,103 @@
+const BaseXform = require('../../base-xform');
+
+const NvSpPrXform = require('./nv-sp-pr-xform');
+const SpPrXform = require('./sp-pr-xform');
+const StyleXform = require('./style-xform');
+const TxBodyXform = require('./tx-body-xform');
+
+// DocumentFormat.OpenXml.Drawing.Spreadsheet.Shape
+class SpXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'xdr:nvSpPr': new NvSpPrXform(),
+ 'xdr:spPr': new SpPrXform(),
+ 'xdr:style': new StyleXform(),
+ 'xdr:txBody': new TxBodyXform(),
+ };
+ }
+
+ get tag() {
+ return 'xdr:sp';
+ }
+
+ prepare(model, options) {
+ model.index = options.index + 1;
+ }
+
+ render(xmlStream, shape) {
+ xmlStream.openNode(this.tag, {macro: '', textlink: ''});
+
+ this.map['xdr:nvSpPr'].render(xmlStream, shape);
+ this.map['xdr:spPr'].render(xmlStream, shape.props);
+ this.map['xdr:style'].render(xmlStream, shape.props);
+ if (shape.props.textBody) {
+ this.map['xdr:txBody'].render(xmlStream, shape.props.textBody);
+ }
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ this.model = {props: {}};
+ return true;
+ }
+ switch (node.name) {
+ case this.tag:
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText(text) {
+ if (this.parser) {
+ this.parser.parseText(text);
+ }
+ }
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ if (this.map['xdr:style'].model) {
+ this.model.props = {
+ ...(this.map['xdr:style'].model.fill ? {fill: this.map['xdr:style'].model.fill} : {}),
+ ...(this.map['xdr:style'].model.outline ? {outline: this.map['xdr:style'].model.outline} : {}),
+ };
+ }
+ if (this.map['xdr:spPr'].model) {
+ this.model.props = {
+ ...this.model.props,
+ ...this.map['xdr:spPr'].model,
+ };
+ }
+ if (this.map['xdr:txBody'].model) {
+ this.model.props.textBody = this.map['xdr:txBody'].model;
+ }
+ if (this.map['xdr:spPr'].noFill) {
+ delete this.model.props.fill;
+ }
+ if (this.map['xdr:nvSpPr'].model) {
+ this.model.hyperlinks = this.map['xdr:nvSpPr'].model.hyperlinks;
+ }
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = SpXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/style-matrix-reference-type-xform.js b/lib/xlsx/xform/drawing/shape/style-matrix-reference-type-xform.js
new file mode 100644
index 0000000..628b869
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/style-matrix-reference-type-xform.js
@@ -0,0 +1,78 @@
+const BaseXform = require('../../base-xform');
+
+// DocumentFormat.OpenXml.Drawing.StyleMatrixReferenceType
+class StyleMatrixReferenceTypeXform extends BaseXform {
+ constructor(tagName) {
+ super();
+
+ this.map = {};
+ this.tagName = tagName;
+ }
+
+ get tag() {
+ return this.tagName;
+ }
+
+ idx() {
+ switch (this.tagName) {
+ case 'a:lnRef':
+ return 2;
+ case 'a:fillRef':
+ return 1;
+ default:
+ // do not know when to come here
+ return 0;
+ }
+ }
+
+ render(xmlStream) {
+ xmlStream.openNode(this.tag, {idx: this.idx()});
+ xmlStream.leafNode('a:schemeClr', {val: 'accent1'});
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {};
+ break;
+ case 'a:schemeClr':
+ this.model.theme = node.attributes.val;
+ break;
+ case 'a:srgbClr':
+ this.model.rgb = node.attributes.val;
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = StyleMatrixReferenceTypeXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/style-xform.js b/lib/xlsx/xform/drawing/shape/style-xform.js
new file mode 100644
index 0000000..149f057
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/style-xform.js
@@ -0,0 +1,101 @@
+const BaseXform = require('../../base-xform');
+const StaticXform = require('../../static-xform');
+const StyleMatrixReferenceTypeXform = require('./style-matrix-reference-type-xform');
+
+// DocumentFormat.OpenXml.Drawing.Spreadsheet.ShapeStyle
+class StyleXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:lnRef': new StyleMatrixReferenceTypeXform('a:lnRef'),
+ 'a:fillRef': new StyleMatrixReferenceTypeXform('a:fillRef'),
+ 'a:effectRef': new StaticXform(effectRefJSON),
+ 'a:fontRef': new StaticXform(fontRefJSON),
+ };
+ }
+
+ get tag() {
+ return 'xdr:style';
+ }
+
+ render(xmlStream, shape) {
+ xmlStream.openNode(this.tag);
+ // Must care about the order
+ this.map['a:lnRef'].render(xmlStream);
+ this.map['a:fillRef'].render(xmlStream);
+ this.map['a:effectRef'].render(xmlStream, shape.effectRef);
+ this.map['a:fontRef'].render(xmlStream, shape.fontRef);
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {};
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ if (this.map['a:lnRef'].model) {
+ this.model.outline = this.map['a:lnRef'].model;
+ }
+ if (this.map['a:fillRef'].model) {
+ this.model.fill = {
+ type: 'solid',
+ color: this.map['a:fillRef'].model,
+ };
+ }
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+const effectRefJSON = {
+ tag: 'a:effectRef',
+ $: {idx: '0'},
+ c: [
+ {
+ tag: 'a:schemeClr',
+ $: {val: 'accent1'},
+ },
+ ],
+};
+
+const fontRefJSON = {
+ tag: 'a:fontRef',
+ $: {idx: 'minor'},
+ c: [
+ {
+ tag: 'a:schemeClr',
+ $: {val: 'lt1'},
+ },
+ ],
+};
+
+module.exports = StyleXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/shape/tx-body-xform.js b/lib/xlsx/xform/drawing/shape/tx-body-xform.js
new file mode 100644
index 0000000..2190008
--- /dev/null
+++ b/lib/xlsx/xform/drawing/shape/tx-body-xform.js
@@ -0,0 +1,86 @@
+const BaseXform = require('../../base-xform');
+const ParagraphXform = require('./p-xform');
+
+// DocumentFormat.OpenXml.Drawing.Spreadsheet.TextBody
+class TxBodyXform extends BaseXform {
+ constructor() {
+ super();
+
+ this.map = {
+ 'a:p': new ParagraphXform(),
+ };
+ }
+
+ get tag() {
+ return 'xdr:txBody';
+ }
+
+ render(xmlStream, textBody) {
+ xmlStream.openNode(this.tag);
+ xmlStream.openNode('a:bodyPr', {
+ vertOverflow: 'clip',
+ horzOverflow: 'clip',
+ rtlCol: 0,
+ });
+ if (textBody.vertAlign) {
+ xmlStream.addAttribute('anchor', textBody.vertAlign);
+ }
+ xmlStream.closeNode();
+ xmlStream.leafNode('a:lstStyle');
+ textBody.paragraphs.forEach(p => {
+ this.map['a:p'].render(xmlStream, p);
+ });
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+
+ switch (node.name) {
+ case this.tag:
+ this.model = {paragraphs: []};
+ break;
+ case 'a:bodyPr':
+ if (node.attributes.anchor) {
+ this.model.vertAlign = node.attributes.anchor;
+ }
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText(text) {
+ if (this.parser) {
+ this.parser.parseText(text);
+ }
+ }
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ if (name === 'a:p') {
+ this.model.paragraphs.push(this.parser.model);
+ }
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = TxBodyXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/drawing/two-cell-anchor-xform.js b/lib/xlsx/xform/drawing/two-cell-anchor-xform.js
index bc567e3..7d91446 100644
--- a/lib/xlsx/xform/drawing/two-cell-anchor-xform.js
+++ b/lib/xlsx/xform/drawing/two-cell-anchor-xform.js
@@ -2,7 +2,8 @@ const BaseCellAnchorXform = require('./base-cell-anchor-xform');
const StaticXform = require('../static-xform');
const CellPositionXform = require('./cell-position-xform');
-const PicXform = require('./pic-xform');
+const PicXform = require('./picture/pic-xform');
+const SpXform = require('./shape/sp-xform');
class TwoCellAnchorXform extends BaseCellAnchorXform {
constructor() {
@@ -12,6 +13,7 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
'xdr:from': new CellPositionXform({tag: 'xdr:from'}),
'xdr:to': new CellPositionXform({tag: 'xdr:to'}),
'xdr:pic': new PicXform(),
+ 'xdr:sp': new SpXform(),
'xdr:clientData': new StaticXform({tag: 'xdr:clientData'}),
};
}
@@ -21,7 +23,12 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
}
prepare(model, options) {
- this.map['xdr:pic'].prepare(model.picture, options);
+ if (model.picture) {
+ this.map['xdr:pic'].prepare(model.picture, options);
+ }
+ if (model.shape) {
+ this.map['xdr:sp'].prepare(model.shape, options);
+ }
}
render(xmlStream, model) {
@@ -29,7 +36,12 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
this.map['xdr:from'].render(xmlStream, model.range.tl);
this.map['xdr:to'].render(xmlStream, model.range.br);
- this.map['xdr:pic'].render(xmlStream, model.picture);
+ if (model.picture) {
+ this.map['xdr:pic'].render(xmlStream, model.picture);
+ }
+ if (model.shape) {
+ this.map['xdr:sp'].render(xmlStream, model.shape);
+ }
this.map['xdr:clientData'].render(xmlStream, {});
xmlStream.closeNode();
@@ -47,6 +59,7 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
this.model.range.tl = this.map['xdr:from'].model;
this.model.range.br = this.map['xdr:to'].model;
this.model.picture = this.map['xdr:pic'].model;
+ this.model.shape = this.map['xdr:sp'].model;
return false;
default:
// could be some unrecognised tags
diff --git a/lib/xlsx/xform/drawing/xfrm-xform.js b/lib/xlsx/xform/drawing/xfrm-xform.js
new file mode 100644
index 0000000..0519ac8
--- /dev/null
+++ b/lib/xlsx/xform/drawing/xfrm-xform.js
@@ -0,0 +1,66 @@
+const BaseXform = require('../base-xform');
+
+// DocumentFormat.OpenXml.Drawing.Transform2D
+class XfrmXform extends BaseXform {
+ constructor() {
+ super();
+ this.map = {};
+ }
+
+ get tag() {
+ return 'a:xfrm';
+ }
+
+ render(xmlStream, model) {
+ xmlStream.openNode(this.tag, {
+ rot: model.rotation ? model.rotation * 60000 : undefined,
+ flipH: model.horizontalFlip ? '1' : undefined,
+ flipV: model.verticalFlip ? '1' : undefined,
+ });
+ xmlStream.leafNode('a:off', {x: 0, y: 0});
+ xmlStream.leafNode('a:ext', {cx: 0, cy: 0});
+ xmlStream.closeNode();
+ }
+
+ parseOpen(node) {
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ return true;
+ }
+ switch (node.name) {
+ case this.tag:
+ this.model = {
+ rotation: node.attributes.rot ? parseInt(node.attributes.rot, 10) / 60000 : undefined,
+ horizontalFlip: node.attributes.flipH ? node.attributes.flipH === '1' : undefined,
+ verticalFlip: node.attributes.flipV ? node.attributes.flipV === '1' : undefined,
+ };
+ break;
+ default:
+ this.parser = this.map[node.name];
+ if (this.parser) {
+ this.parser.parseOpen(node);
+ }
+ break;
+ }
+ return true;
+ }
+
+ parseText() {}
+
+ parseClose(name) {
+ if (this.parser) {
+ if (!this.parser.parseClose(name)) {
+ this.parser = undefined;
+ }
+ return true;
+ }
+ switch (name) {
+ case this.tag:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
+
+module.exports = XfrmXform;
\ No newline at end of file
diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js
index 490f384..7cf7ce6 100644
--- a/lib/xlsx/xform/sheet/worksheet-xform.js
+++ b/lib/xlsx/xform/sheet/worksheet-xform.js
@@ -199,6 +199,37 @@ class WorkSheetXform extends BaseXform {
});
}
+ const getDrawingModel = () => {
+ if (!model.drawing) {
+ const drawing = {
+ rId: nextRid(rels),
+ name: `drawing${++options.drawingsCount}`,
+ anchors: [],
+ rels: [],
+ };
+ options.drawings.push(drawing);
+ rels.push({
+ Id: drawing.rId,
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ Target: `../drawings/${drawing.name}.xml`,
+ });
+ model.drawing = drawing;
+ }
+ return model.drawing;
+ };
+
+ const addHyperlink = (hyperlink, drawing) => {
+ const rId = nextRid(drawing.rels);
+ drawingRelsHash[drawing.rels.length] = rId;
+ drawing.rels.push({
+ Id: rId,
+ Type: RelType.Hyperlink,
+ Target: hyperlink,
+ TargetMode: 'External',
+ });
+ return rId;
+ };
+
const drawingRelsHash = [];
let bookImage;
model.media.forEach(medium => {
@@ -215,22 +246,8 @@ class WorkSheetXform extends BaseXform {
};
model.image = options.media[medium.imageId];
} else if (medium.type === 'image') {
- let {drawing} = model;
bookImage = options.media[medium.imageId];
- if (!drawing) {
- drawing = model.drawing = {
- rId: nextRid(rels),
- name: `drawing${++options.drawingsCount}`,
- anchors: [],
- rels: [],
- };
- options.drawings.push(drawing);
- rels.push({
- Id: drawing.rId,
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
- Target: `../drawings/${drawing.name}.xml`,
- });
- }
+ const drawing = getDrawingModel();
let rIdImage =
this.preImageId === medium.imageId ? drawingRelsHash[medium.imageId] : drawingRelsHash[drawing.rels.length];
if (!rIdImage) {
@@ -250,24 +267,35 @@ class WorkSheetXform extends BaseXform {
range: medium.range,
};
if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
- const rIdHyperLink = nextRid(drawing.rels);
- drawingRelsHash[drawing.rels.length] = rIdHyperLink;
+ const rId = addHyperlink(medium.hyperlinks.hyperlink, drawing);
anchor.picture.hyperlinks = {
tooltip: medium.hyperlinks.tooltip,
- rId: rIdHyperLink,
+ rId,
};
- drawing.rels.push({
- Id: rIdHyperLink,
- Type: RelType.Hyperlink,
- Target: medium.hyperlinks.hyperlink,
- TargetMode: 'External',
- });
}
this.preImageId = medium.imageId;
drawing.anchors.push(anchor);
}
});
+ model.shapes.forEach(shape => {
+ const drawing = getDrawingModel();
+ const anchor = {
+ shape: {
+ props: shape.props,
+ },
+ range: shape.range,
+ };
+ if (shape.hyperlinks && shape.hyperlinks.hyperlink) {
+ const rId = addHyperlink(shape.hyperlinks.hyperlink, drawing);
+ anchor.shape.hyperlinks = {
+ tooltip: shape.hyperlinks.tooltip,
+ rId,
+ };
+ }
+ drawing.anchors.push(anchor);
+ });
+
// prepare tables
model.tables.forEach(table => {
// relationships
@@ -494,6 +522,7 @@ class WorkSheetXform extends BaseXform {
this.map.conditionalFormatting.reconcile(model.conditionalFormattings, options);
model.media = [];
+ model.shapes = [];
if (model.drawing) {
const drawingRel = rels[model.drawing.rId];
const match = drawingRel.Target.match(/\/drawings\/([a-zA-Z0-9]+)[.][a-zA-Z]{3,4}$/);
@@ -509,6 +538,13 @@ class WorkSheetXform extends BaseXform {
hyperlinks: anchor.picture.hyperlinks,
};
model.media.push(image);
+ } else if (anchor.shape) {
+ const shape = {
+ props: anchor.shape.props,
+ range: anchor.range,
+ hyperlinks: anchor.shape.hyperlinks,
+ };
+ model.shapes.push(shape);
}
});
}
diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js
index 43dee23..1496a47 100644
--- a/lib/xlsx/xlsx.js
+++ b/lib/xlsx/xlsx.js
@@ -100,7 +100,7 @@ class XLSX {
return o;
}, {});
(drawing.anchors || []).forEach(anchor => {
- const hyperlinks = anchor.picture && anchor.picture.hyperlinks;
+ const hyperlinks = (anchor.picture && anchor.picture.hyperlinks) || (anchor.shape && anchor.shape.hyperlinks);
if (hyperlinks && drawingOptions.rels[hyperlinks.rId]) {
hyperlinks.hyperlink = drawingOptions.rels[hyperlinks.rId].Target;
delete hyperlinks.rId;
diff --git a/package.json b/package.json
index 5af78a3..4e86ca6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@zurmokeeper/exceljs",
- "version": "4.4.7",
+ "version": "4.4.8",
"description": "Excel Workbook Manager - Read and Write xlsx and csv Files.",
"private": false,
"license": "MIT",
diff --git a/spec/integration/workbook/shapes.spec.js b/spec/integration/workbook/shapes.spec.js
new file mode 100644
index 0000000..fb0a72a
--- /dev/null
+++ b/spec/integration/workbook/shapes.spec.js
@@ -0,0 +1,166 @@
+const ExcelJS = verquire('exceljs');
+
+const TEST_XLSX_FILE_NAME = './spec/out/wb.test.xlsx';
+
+// =============================================================================
+// Tests
+
+describe('Workbook', () => {
+ describe('Shapes', () => {
+ it('stores shape', () => {
+ const wb = new ExcelJS.Workbook();
+ const ws = wb.addWorksheet('sheet');
+ let wb2;
+ let ws2;
+
+ ws.addShape(
+ {
+ type: 'line',
+ fill: {type: 'solid', color: {theme: 'accent6'}},
+ outline: {
+ weight: 30000,
+ color: {theme: 'accent1'},
+ arrow: {
+ head: {type: 'triangle', width: 'lg', length: 'med'},
+ },
+ },
+ },
+ 'B2:D6'
+ );
+
+ return wb.xlsx
+ .writeFile(TEST_XLSX_FILE_NAME)
+ .then(() => {
+ wb2 = new ExcelJS.Workbook();
+ return wb2.xlsx.readFile(TEST_XLSX_FILE_NAME);
+ })
+ .then(() => {
+ ws2 = wb2.getWorksheet('sheet');
+ expect(ws2).to.not.be.undefined();
+ const shapes = ws2.getShapes();
+ expect(shapes.length).to.equal(1);
+
+ const shape = shapes[0];
+ expect(shape.props.type).to.equal('line');
+ expect(shape.props.fill).to.deep.equal({
+ type: 'solid',
+ color: {theme: 'accent6'},
+ });
+ expect(shape.props.outline).to.deep.equal({
+ weight: 30000,
+ color: {theme: 'accent1'},
+ arrow: {
+ head: {type: 'triangle', width: 'lg', length: 'med'},
+ },
+ });
+ });
+ });
+
+ it('stores shape with oneCell', () => {
+ const wb = new ExcelJS.Workbook();
+ const ws = wb.addWorksheet('sheet');
+ let wb2;
+ let ws2;
+
+ ws.addShape(
+ {
+ type: 'rect',
+ rotation: 180,
+ horizontalFlip: true,
+ fill: {type: 'solid', color: {rgb: 'AABBCC'}},
+ },
+ {
+ tl: {col: 0.1125, row: 0.4},
+ br: {col: 2.101046875, row: 3.4},
+ editAs: 'oneCell',
+ }
+ );
+
+ return wb.xlsx
+ .writeFile(TEST_XLSX_FILE_NAME)
+ .then(() => {
+ wb2 = new ExcelJS.Workbook();
+ return wb2.xlsx.readFile(TEST_XLSX_FILE_NAME);
+ })
+ .then(() => {
+ ws2 = wb2.getWorksheet('sheet');
+ expect(ws2).to.not.be.undefined();
+ const shapes = ws2.getShapes();
+ expect(shapes.length).to.equal(1);
+
+ const shape = shapes[0];
+ expect(shape.range.editAs).to.equal('oneCell');
+ expect(shape.props.type).to.equal('rect');
+ expect(shape.props.rotation).to.equal(180);
+ expect(shape.props.horizontalFlip).to.equal(true);
+ expect(shape.props.fill).to.deep.equal({
+ type: 'solid',
+ color: {rgb: 'AABBCC'},
+ });
+ });
+ });
+ });
+});
+
+describe('Parsing text body', () => {
+ function addAndGetShapeWithTextBody(textBody) {
+ const wb = new ExcelJS.Workbook();
+ const ws = wb.addWorksheet();
+ ws.addShape(
+ {
+ textBody,
+ type: 'rect',
+ },
+ 'B2:D6'
+ );
+ return ws.getShapes()[0];
+ }
+
+ it('single string', () => {
+ const shape = addAndGetShapeWithTextBody('foo');
+ expect(shape.props.textBody).to.deep.equal({
+ paragraphs: [{runs: [{text: 'foo'}]}],
+ });
+ });
+ it('array of strings', () => {
+ const shape = addAndGetShapeWithTextBody(['foo', 'bar']);
+ expect(shape.props.textBody).to.deep.equal({
+ paragraphs: [{runs: [{text: 'foo'}]}, {runs: [{text: 'bar'}]}],
+ });
+ });
+ it('array of array of strings', () => {
+ const shape = addAndGetShapeWithTextBody([
+ ['foo', 'bar'],
+ ['baz', 'qux'],
+ ]);
+ expect(shape.props.textBody).to.deep.equal({
+ paragraphs: [
+ {runs: [{text: 'foo'}, {text: 'bar'}]},
+ {runs: [{text: 'baz'}, {text: 'qux'}]},
+ ],
+ });
+ });
+ it('object', () => {
+ const obj = {
+ paragraphs: [
+ {
+ runs: [
+ {
+ text: 'foo',
+ font: {size: 15, bold: true, italic: true, underline: 'sng'},
+ },
+ {
+ text: 'bar',
+ font: {color: {theme: 'accent1'}},
+ },
+ ],
+ alignment: 'ctr',
+ },
+ {runs: [{text: 'baz'}, {text: 'qux'}]},
+ ],
+ vertAlign: 'b',
+ };
+ const shape = addAndGetShapeWithTextBody(obj);
+ expect(shape.props.textBody).to.deep.equal(obj);
+ });
+});
\ No newline at end of file
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.1.3.js b/spec/unit/xlsx/xform/drawing/data/drawing.1.3.js
index ab17dcc..2110fe9 100644
--- a/spec/unit/xlsx/xform/drawing/data/drawing.1.3.js
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.1.3.js
@@ -13,6 +13,7 @@ module.exports = {
rId: 'rId3',
},
},
+ shape: null,
},
{
range: {
@@ -28,6 +29,7 @@ module.exports = {
picture: {
rId: 'rId2',
},
+ shape: null,
},
],
};
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.1.4.js b/spec/unit/xlsx/xform/drawing/data/drawing.1.4.js
index ab17dcc..2110fe9 100644
--- a/spec/unit/xlsx/xform/drawing/data/drawing.1.4.js
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.1.4.js
@@ -13,6 +13,7 @@ module.exports = {
rId: 'rId3',
},
},
+ shape: null,
},
{
range: {
@@ -28,6 +29,7 @@ module.exports = {
picture: {
rId: 'rId2',
},
+ shape: null,
},
],
};
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.2.0.js b/spec/unit/xlsx/xform/drawing/data/drawing.2.0.js
new file mode 100644
index 0000000..89f1f8d
--- /dev/null
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.2.0.js
@@ -0,0 +1,75 @@
+module.exports = {
+ anchors: [
+ {
+ range: {
+ tl: {
+ nativeRow: 4,
+ nativeRowOff: 165100,
+ nativeCol: 1,
+ nativeColOff: 647700,
+ },
+ br: {
+ nativeRow: 10,
+ nativeRowOff: 165100,
+ nativeCol: 4,
+ nativeColOff: 508000,
+ },
+ editAs: 'oneCell',
+ },
+ shape: {
+ props: {
+ type: 'rect',
+ fill: {
+ type: 'solid',
+ color: {
+ theme: 'accent6',
+ },
+ },
+ textBody: {
+ vertAlign: 'b',
+ paragraphs: [
+ {
+ alignment: 'l',
+ runs: [
+ {
+ font: {size: 11},
+ text: 'Shape1',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ range: {
+ tl: {
+ nativeCol: 6,
+ nativeColOff: 101600,
+ nativeRow: 12,
+ nativeRowOff: 63500,
+ },
+ br: {
+ nativeCol: 7,
+ nativeColOff: 190500,
+ nativeRow: 16,
+ nativeRowOff: 165100,
+ },
+ editAs: 'oneCell',
+ },
+ picture: null,
+ shape: {
+ props: {
+ type: 'ellipse',
+ fill: {
+ type: 'solid',
+ color: {
+ rgb: 'C651E9',
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
\ No newline at end of file
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.2.1.js b/spec/unit/xlsx/xform/drawing/data/drawing.2.1.js
new file mode 100644
index 0000000..25acf3c
--- /dev/null
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.2.1.js
@@ -0,0 +1,79 @@
+module.exports = {
+ anchors: [
+ {
+ anchorType: 'xdr:twoCellAnchor',
+ range: {
+ tl: {
+ nativeRow: 4,
+ nativeRowOff: 165100,
+ nativeCol: 1,
+ nativeColOff: 647700,
+ },
+ br: {
+ nativeRow: 10,
+ nativeRowOff: 165100,
+ nativeCol: 4,
+ nativeColOff: 508000,
+ },
+ editAs: 'oneCell',
+ },
+ shape: {
+ index: 1,
+ props: {
+ type: 'rect',
+ fill: {
+ type: 'solid',
+ color: {
+ theme: 'accent6',
+ },
+ },
+ textBody: {
+ vertAlign: 'b',
+ paragraphs: [
+ {
+ alignment: 'l',
+ runs: [
+ {
+ font: {size: 11},
+ text: 'Shape1',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ anchorType: 'xdr:twoCellAnchor',
+ range: {
+ tl: {
+ nativeCol: 6,
+ nativeColOff: 101600,
+ nativeRow: 12,
+ nativeRowOff: 63500,
+ },
+ br: {
+ nativeCol: 7,
+ nativeColOff: 190500,
+ nativeRow: 16,
+ nativeRowOff: 165100,
+ },
+ editAs: 'oneCell',
+ },
+ picture: null,
+ shape: {
+ index: 2,
+ props: {
+ type: 'ellipse',
+ fill: {
+ type: 'solid',
+ color: {
+ rgb: 'C651E9',
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
\ No newline at end of file
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.2.2.xml b/spec/unit/xlsx/xform/drawing/data/drawing.2.2.xml
new file mode 100644
index 0000000..d337571
--- /dev/null
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.2.2.xml
@@ -0,0 +1,121 @@
+
+
+
+
+ 1
+ 647700
+ 4
+ 165100
+
+
+ 4
+ 508000
+ 10
+ 165100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shape1
+
+
+
+
+
+
+
+
+ 6
+ 101600
+ 12
+ 63500
+
+
+ 7
+ 190500
+ 16
+ 165100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spec/unit/xlsx/xform/drawing/data/drawing.2.3.js b/spec/unit/xlsx/xform/drawing/data/drawing.2.3.js
new file mode 100644
index 0000000..712d7b9
--- /dev/null
+++ b/spec/unit/xlsx/xform/drawing/data/drawing.2.3.js
@@ -0,0 +1,82 @@
+module.exports = {
+ anchors: [
+ {
+ range: {
+ tl: {
+ nativeRow: 4,
+ nativeRowOff: 165100,
+ nativeCol: 1,
+ nativeColOff: 647700,
+ },
+ br: {
+ nativeRow: 10,
+ nativeRowOff: 165100,
+ nativeCol: 4,
+ nativeColOff: 508000,
+ },
+ editAs: 'oneCell',
+ },
+ picture: null,
+ shape: {
+ props: {
+ type: 'rect',
+ fill: {
+ type: 'solid',
+ color: {
+ theme: 'accent6',
+ },
+ },
+ outline: {
+ theme: 'accent1',
+ },
+ textBody: {
+ vertAlign: 'b',
+ paragraphs: [
+ {
+ alignment: 'l',
+ runs: [
+ {
+ font: {size: 11},
+ text: 'Shape1',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ range: {
+ tl: {
+ nativeCol: 6,
+ nativeColOff: 101600,
+ nativeRow: 12,
+ nativeRowOff: 63500,
+ },
+ br: {
+ nativeCol: 7,
+ nativeColOff: 190500,
+ nativeRow: 16,
+ nativeRowOff: 165100,
+ },
+ editAs: 'oneCell',
+ },
+ picture: null,
+ shape: {
+ props: {
+ type: 'ellipse',
+ fill: {
+ type: 'solid',
+ color: {
+ rgb: 'C651E9',
+ },
+ },
+ outline: {
+ theme: 'accent1',
+ },
+ },
+ },
+ },
+ ],
+ };
\ No newline at end of file
diff --git a/spec/unit/xlsx/xform/drawing/drawing-xform.spec.js b/spec/unit/xlsx/xform/drawing/drawing-xform.spec.js
index c5ee02e..0865f5a 100644
--- a/spec/unit/xlsx/xform/drawing/drawing-xform.spec.js
+++ b/spec/unit/xlsx/xform/drawing/drawing-xform.spec.js
@@ -27,6 +27,19 @@ const expectations = [
tests: ['prepare', 'render', 'renderIn', 'parse', 'reconcile'],
options,
},
+ {
+ title: 'Drawing 2 (Shapes)',
+ create() {
+ return new DrawingXform();
+ },
+ initialModel: require('./data/drawing.2.0.js'),
+ preparedModel: require('./data/drawing.2.1.js'),
+ xml: fs.readFileSync(`${__dirname}/data/drawing.2.2.xml`).toString(),
+ parsedModel: require('./data/drawing.2.3.js'),
+ reconciledModel: require('./data/drawing.2.3.js'),
+ tests: ['prepare', 'render', 'renderIn', 'parse', 'reconcile'],
+ options,
+ },
];
describe('DrawingXform', () => {
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json b/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json
index a86cfd2..66392cd 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json
@@ -72,6 +72,7 @@
"evenFooter": "&Lexceljs&C&F&RPage &P evenHeader"
},
"media": [],
+ "shapes": [],
"rowBreaks": [],
"colBreaks": [],
"tables": [],
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json b/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json
index 339392a..688e199 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json
@@ -78,6 +78,7 @@
],
"comments": [],
"media": [],
+ "shapes": [],
"rowBreaks": [],
"colBreaks": [],
"tables": [],
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.2.0.json b/spec/unit/xlsx/xform/sheet/data/sheet.2.0.json
index 6c3ab3c..f51da13 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.2.0.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.2.0.json
@@ -45,6 +45,7 @@
"B7": { "type": "whole", "allowBlank": true, "showInputMessage": true, "showErrorMessage": true, "operator": "between", "formulae": ["1", "10"]}
},
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": []
}
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.2.1.json b/spec/unit/xlsx/xform/sheet/data/sheet.2.1.json
index f905eef..d1a8468 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.2.1.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.2.1.json
@@ -49,6 +49,7 @@
},
"comments": [],
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": []
}
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.4.0.json b/spec/unit/xlsx/xform/sheet/data/sheet.4.0.json
index 49ad14f..91b3b03 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.4.0.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.4.0.json
@@ -45,6 +45,7 @@
"B7": { "type": "whole", "allowBlank": true, "showInputMessage": true, "showErrorMessage": true, "operator": "between", "formulae": ["1", "10"]}
},
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": [{
"ref": "A1:E7",
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.5.0.json b/spec/unit/xlsx/xform/sheet/data/sheet.5.0.json
index 5c86483..c2a78d5 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.5.0.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.5.0.json
@@ -11,6 +11,7 @@
}
],
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": []
}
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.7.0.json b/spec/unit/xlsx/xform/sheet/data/sheet.7.0.json
index 0287553..d9a52b1 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.7.0.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.7.0.json
@@ -47,6 +47,7 @@
{ "id": 3, "min": 0, "max": 1000, "man": 1 }
],
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": []
}
diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.7.1.json b/spec/unit/xlsx/xform/sheet/data/sheet.7.1.json
index db2436a..e3adb45 100644
--- a/spec/unit/xlsx/xform/sheet/data/sheet.7.1.json
+++ b/spec/unit/xlsx/xform/sheet/data/sheet.7.1.json
@@ -51,6 +51,7 @@
],
"comments": [],
"media": [],
+ "shapes": [],
"tables": [],
"conditionalFormattings": []
}