From 5a30249aa8cfd2af6877644f3307f240c14496ab Mon Sep 17 00:00:00 2001 From: baian1 Date: Fri, 20 Oct 2023 19:08:42 +0800 Subject: [PATCH 1/6] feat: header and footer support image --- .prettier => .prettierrc.json | 0 index.d.ts | 6 + lib/doc/worksheet.js | 46 ++++- .../xform/comment/vml-client-data-xform.js | 3 + lib/xlsx/xform/comment/vml-shape-xform.js | 1 + lib/xlsx/xform/core/content-types-xform.js | 7 + .../xform/drawing/vlm-drawing/o-lock-xform.js | 39 ++++ .../drawing/vlm-drawing/vml-drawing-xform.js | 110 +++++++++++ .../drawing/vlm-drawing/vml-fill-xform.js | 45 +++++ .../drawing/vlm-drawing/vml-image.xform.js | 55 ++++++ .../drawing/vlm-drawing/vml-path-xform.js | 40 ++++ .../drawing/vlm-drawing/vml-shadow-xform.js | 45 +++++ .../drawing/vlm-drawing/vml-shap-xform.js | 182 ++++++++++++++++++ .../drawing/vlm-drawing/vml-stroke-xform.js | 40 ++++ .../xform/sheet/legacy-drawingHF-xform.js | 31 +++ lib/xlsx/xform/sheet/worksheet-xform.js | 90 +++++++-- lib/xlsx/xlsx.js | 59 +++++- .../vml-drawing/vml-drawing-xform.spec.js | 153 +++++++++++++++ .../unit/xlsx/xform/sheet/data/sheet.1.0.json | 13 ++ .../unit/xlsx/xform/sheet/data/sheet.1.1.json | 33 ++++ spec/unit/xlsx/xform/sheet/data/sheet.1.2.xml | 3 +- .../unit/xlsx/xform/sheet/data/sheet.1.3.json | 3 + .../unit/xlsx/xform/sheet/data/sheet.5.3.json | 1 + .../unit/xlsx/xform/sheet/data/sheet.6.3.json | 1 + .../xlsx/xform/sheet/worksheet-xform.spec.js | 6 + 25 files changed, 982 insertions(+), 30 deletions(-) rename .prettier => .prettierrc.json (100%) create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/o-lock-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-drawing-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-fill-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-image.xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-path-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-shadow-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-shap-xform.js create mode 100644 lib/xlsx/xform/drawing/vlm-drawing/vml-stroke-xform.js create mode 100644 lib/xlsx/xform/sheet/legacy-drawingHF-xform.js create mode 100644 spec/unit/xlsx/xform/drawing/vml-drawing/vml-drawing-xform.spec.js diff --git a/.prettier b/.prettierrc.json similarity index 100% rename from .prettier rename to .prettierrc.json diff --git a/index.d.ts b/index.d.ts index 81e65fec4..058883fd6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1345,6 +1345,12 @@ export interface Worksheet { range: ImageRange; }>; + /** + * footer heaedr + */ + addHFImage(imageId: number, options: { id: string, width: string, height: string }): void + + commit(): void; model: WorksheetModel; diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 1855b499e..9eff826cc 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -109,6 +109,8 @@ class Worksheet { options.headerFooter ); + this.hfImages = []; + this.dataValidations = new DataValidations(); // for freezepanes, split, zoom, gridlines, etc @@ -156,11 +158,15 @@ class Worksheet { // Illegal character in worksheet name: asterisk (*), question mark (?), // colon (:), forward slash (/ \), or bracket ([]) if (/[*?:/\\[\]]/.test(name)) { - throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`); + throw new Error( + `Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]` + ); } if (/(^')|('$)/.test(name)) { - throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`); + throw new Error( + `The first or last character of worksheet name cannot be a single quotation mark: ${name}` + ); } if (name && name.length > 31) { @@ -529,7 +535,9 @@ class Worksheet { if (cell._value.constructor.name === 'MergeValue') { const cellToBeMerged = this.getRow(cell._row._number + nInserts).getCell(colNumber); const prevMaster = cell._value._master; - const newMaster = this.getRow(prevMaster._row._number + nInserts).getCell(prevMaster._column._number); + const newMaster = this.getRow(prevMaster._row._number + nInserts).getCell( + prevMaster._column._number + ); cellToBeMerged.merge(newMaster); } }); @@ -751,6 +759,21 @@ class Worksheet { return image && image.imageId; } + addFHImage(imageId, options) { + const model = { + type: 'hfimage', + id: options.id, + style: { + width: options.width, + height: options.height, + }, + imagedata: { + index: imageId, + }, + }; + this.hfImages.push(model); + } + // ========================================================================= // Worksheet Protection protect(password, options) { @@ -762,12 +785,15 @@ class Worksheet { }; if (options && 'spinCount' in options) { // force spinCount to be integer >= 0 - options.spinCount = Number.isFinite(options.spinCount) ? Math.round(Math.max(0, options.spinCount)) : 100000; + options.spinCount = Number.isFinite(options.spinCount) + ? Math.round(Math.max(0, options.spinCount)) + : 100000; } if (password) { this.sheetProtection.algorithmName = 'SHA-512'; this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64'); - this.sheetProtection.spinCount = options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount + this.sheetProtection.spinCount = + options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount this.sheetProtection.hashValue = Encryptor.convertPasswordToHash( password, 'SHA512', @@ -846,13 +872,17 @@ Please leave feedback at https://github.com/exceljs/exceljs/discussions/2575` // Deprecated get tabColor() { // eslint-disable-next-line no-console - console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor'); + console.trace( + 'worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor' + ); return this.properties.tabColor; } set tabColor(value) { // eslint-disable-next-line no-console - console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor'); + console.trace( + 'worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor' + ); this.properties.tabColor = value; } @@ -868,6 +898,7 @@ Please leave feedback at https://github.com/exceljs/exceljs/discussions/2575` state: this.state, pageSetup: this.pageSetup, headerFooter: this.headerFooter, + hfImages: this.hfImages, rowBreaks: this.rowBreaks, views: this.views, autoFilter: this.autoFilter, @@ -943,6 +974,7 @@ Please leave feedback at https://github.com/exceljs/exceljs/discussions/2575` }, {}); this.pivotTables = value.pivotTables; this.conditionalFormattings = value.conditionalFormattings; + this.hfImages = value.hfImages || []; } } diff --git a/lib/xlsx/xform/comment/vml-client-data-xform.js b/lib/xlsx/xform/comment/vml-client-data-xform.js index f09a3c7fd..5b5d6c1dc 100644 --- a/lib/xlsx/xform/comment/vml-client-data-xform.js +++ b/lib/xlsx/xform/comment/vml-client-data-xform.js @@ -23,6 +23,9 @@ class VmlClientDataXform extends BaseXform { } render(xmlStream, model) { + if (!model.note) { + return; + } const {protection, editAs} = model.note; xmlStream.openNode(this.tag, {ObjectType: 'Note'}); this.map['x:MoveWithCells'].render(xmlStream, editAs, POSITION_TYPE); diff --git a/lib/xlsx/xform/comment/vml-shape-xform.js b/lib/xlsx/xform/comment/vml-shape-xform.js index 6eb0bfeba..0d0eb2e5a 100644 --- a/lib/xlsx/xform/comment/vml-shape-xform.js +++ b/lib/xlsx/xform/comment/vml-shape-xform.js @@ -43,6 +43,7 @@ class VmlShapeXform extends BaseXform { anchor: '', editAs: '', protection: {}, + attributes: node.attributes, }; break; default: diff --git a/lib/xlsx/xform/core/content-types-xform.js b/lib/xlsx/xform/core/content-types-xform.js index 5e8ff5564..065feb617 100644 --- a/lib/xlsx/xform/core/content-types-xform.js +++ b/lib/xlsx/xform/core/content-types-xform.js @@ -105,6 +105,13 @@ class ContentTypesXform extends BaseXform { }); } + if (model.vmlDrawings && model.vmlDrawings.length > 0) { + xmlStream.leafNode('Default', { + Extension: 'vml', + ContentType: 'application/vnd.openxmlformats-officedocument.vmlDrawing', + }); + } + xmlStream.leafNode('Override', { PartName: '/docProps/core.xml', ContentType: 'application/vnd.openxmlformats-package.core-properties+xml', diff --git a/lib/xlsx/xform/drawing/vlm-drawing/o-lock-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/o-lock-xform.js new file mode 100644 index 000000000..3b2db2ead --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/o-lock-xform.js @@ -0,0 +1,39 @@ +const BaseXform = require('../../base-xform'); + +class OLockform extends BaseXform { + get tag() { + return 'o:lock'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + 'v:ext': 'edit', + rotation: 't', + aspectratio: 't', + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.model = {}; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } +} + +module.exports = OLockform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-drawing-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-drawing-xform.js new file mode 100644 index 000000000..878adf190 --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-drawing-xform.js @@ -0,0 +1,110 @@ +const XmlStream = require('../../../../utils/xml-stream'); +const VmlShapeXform = require('./vml-shap-xform'); +const BaseXform = require('../../base-xform'); + +class VmlDrawingXform extends BaseXform { + constructor() { + super(); + this.map = { + 'v:shape': new VmlShapeXform(), + }; + } + + get tag() { + return 'xml'; + } + + render(xmlStream, model) { + xmlStream.openXml(XmlStream.StdDocAttributes); + xmlStream.openNode(this.tag, VmlDrawingXform.DRAWING_ATTRIBUTES); + + xmlStream.openNode('o:shapelayout', {'v:ext': 'edit'}); + xmlStream.leafNode('o:idmap', {'v:ext': 'edit', data: 1}); + xmlStream.closeNode(); + + xmlStream.openNode('v:shapetype', { + id: '_x0000_t202', + coordsize: '21600,21600', + 'o:spt': 202, + path: 'm,l,21600r21600,l21600,xe', + }); + xmlStream.leafNode('v:stroke', {joinstyle: 'miter'}); + xmlStream.leafNode('v:path', { + gradientshapeok: 't', + 'o:connecttype': 'rect', + }); + xmlStream.closeNode(); + + [...(model.comments || []), ...(model.hfImages || [])].forEach((item, index) => { + this.map['v:shape'].render(xmlStream, item, index); + }); + + xmlStream.closeNode(); + } + + parseOpen(node) { + if (this.parser) { + this.parser.parseOpen(node); + return true; + } + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + comments: [], + hfImages: [], + }; + 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 (this.parser.model.type === 'hfimage') { + this.model.hfImages.push(this.parser.model); + } else { + this.model.comments.push(this.parser.model); + } + this.parser = undefined; + } + return true; + } + switch (name) { + case this.tag: + return false; + default: + // could be some unrecognised tags + return true; + } + } + + reconcile(model, options) { + model.hfImages.forEach(hfImage => { + this.map['v:shape'].reconcile(hfImage, options); + }); + } +} + +VmlDrawingXform.DRAWING_ATTRIBUTES = { + 'xmlns:oa': 'urn:schemas-microsoft-com:office:activation', + 'xmlns:p': 'urn:schemas-microsoft-com:office:powerpoint', + 'xmlns:x': 'urn:schemas-microsoft-com:office:excel', + 'xmlns:o': 'urn:schemas-microsoft-com:office:office', + 'xmlns:v': 'urn:schemas-microsoft-com:vml', +}; + +module.exports = VmlDrawingXform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-fill-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-fill-xform.js new file mode 100644 index 000000000..daa780cd9 --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-fill-xform.js @@ -0,0 +1,45 @@ +const BaseXform = require('../../base-xform'); + +class VmlFillform extends BaseXform { + get tag() { + return 'v:fill'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + focussize: model.focussize, + on: model.on, + color2: model.color2, + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + // 'f' + on: node.attributes.on, + focussize: node.attributes.focussize, // '0.0' + color2: node.attributes.color2, + }; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } +} + +module.exports = VmlFillform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-image.xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-image.xform.js new file mode 100644 index 000000000..6019dcc5f --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-image.xform.js @@ -0,0 +1,55 @@ +const BaseXform = require('../../base-xform'); + +class VmlImageform extends BaseXform { + get tag() { + return 'v:imagedata'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + 'o:relid': model.rId, + 'o:title': model.title, + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + rId: node.attributes['o:relid'], + title: node.attributes['o:title'], + }; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } + + reconcile(model, options) { + if (model && model.rId) { + const rel = options.rels[model.rId]; + const match = rel.Target.match(/.*\/media\/(.+[.][a-zA-Z]{3,4})/); + if (match) { + const name = match[1]; + const mediaId = options.mediaIndex[name]; + model.index = options.media[mediaId].index; + } + } + return undefined; + } +} + +module.exports = VmlImageform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-path-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-path-xform.js new file mode 100644 index 000000000..73eaa3b45 --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-path-xform.js @@ -0,0 +1,40 @@ +const BaseXform = require('../../base-xform'); + +class VmlPathform extends BaseXform { + get tag() { + return 'v:path'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + 'o:connecttype': model.connecttype, + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + connecttype: node.attributes['o:connecttype'], + }; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } +} + +module.exports = VmlPathform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-shadow-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-shadow-xform.js new file mode 100644 index 000000000..f3c332f9d --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-shadow-xform.js @@ -0,0 +1,45 @@ +const BaseXform = require('../../base-xform'); + +class VmlShadowform extends BaseXform { + get tag() { + return 'v:shadow'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + on: model.on || 'f', + focussize: model.focussize || '0,0', + color2: model.color2 || 'infoBackground [80]', + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + // 'f' + on: node.attributes.on, + focussize: node.attributes.focussize, // '0.0' + color2: node.attributes.color2, + }; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } +} + +module.exports = VmlShadowform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-shap-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-shap-xform.js new file mode 100644 index 000000000..e5e5cad03 --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-shap-xform.js @@ -0,0 +1,182 @@ +const BaseXform = require('../../base-xform'); +const VmlTextboxXform = require('../../comment/vml-textbox-xform'); +const VmlClientDataXform = require('../../comment/vml-client-data-xform'); +const VmlPathform = require('./vml-path-xform'); +const VmlFillform = require('./vml-fill-xform'); +const VmlStrokeform = require('./vml-stroke-xform'); +const VmlImageform = require('./vml-image.xform'); +const OLockform = require('./o-lock-xform'); +const VmlShadowform = require('./vml-shadow-xform'); + +const VmlShapeType = { + commont: 'commont', + hfimage: 'hfimage', +}; + +class VShapeXform extends BaseXform { + constructor() { + super(); + this.map = { + 'v:textbox': new VmlTextboxXform(), + 'x:ClientData': new VmlClientDataXform(), + 'v:path': new VmlPathform(), + 'v:fill': new VmlFillform(), + 'v:stroke': new VmlStrokeform(), + 'v:imagedata': new VmlImageform(), + 'v:shadow': new VmlShadowform(), + 'o:lock': new OLockform(), + }; + } + + get tag() { + return 'v:shape'; + } + + render(xmlStream, model, index) { + xmlStream.openNode('v:shape', VShapeXform.V_SHAPE_ATTRIBUTES(model, index)); + if (model.shadow) { + this.map['v:shadow'].render(xmlStream, model.shadow); + } + if (model.path) { + this.map['v:path'].render(xmlStream, model.path); + } + if (model.fill) { + this.map['v:fill'].render(xmlStream, model.fill); + } + if (model.stroke) { + this.map['v:stroke'].render(xmlStream, model.stroke); + } + if (model.imagedata) { + this.map['v:imagedata'].render(xmlStream, model.imagedata); + } + if (model.stroke) { + this.map['o:lock'].render(xmlStream, model.stroke); + } + + if (model.margins && model.margins.inset) { + this.map['v:textbox'].render(xmlStream, model); + } + + this.map['x:ClientData'].render(xmlStream, model); + + xmlStream.closeNode(); + } + + parseOpen(node) { + if (this.parser) { + this.parser.parseOpen(node); + return true; + } + + const isHF = node.attributes.type === '#_x0000_t75'; + switch (node.name) { + case this.tag: + this.reset(); + if (isHF) { + const styleObj = node.attributes.style.split(';').reduce((obj, str) => { + if (str === '') { + return obj; + } + const [key, value] = str.split(':'); + obj[key] = value; + return obj; + }, {}); + this.model = { + type: VmlShapeType.hfimage, + id: node.attributes.id, + style: styleObj, + }; + } else { + this.model = { + type: VmlShapeType.commont, + margins: { + insetmode: node.attributes['o:insetmode'], + }, + anchor: '', + editAs: '', + protection: {}, + }; + } + 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.model.margins) { + this.model.margins.inset = + this.map['v:textbox'].model && this.map['v:textbox'].model.inset; + } + this.model.protection = + this.map['x:ClientData'].model && this.map['x:ClientData'].model.protection; + this.model.anchor = this.map['x:ClientData'].model && this.map['x:ClientData'].model.anchor; + this.model.editAs = this.map['x:ClientData'].model && this.map['x:ClientData'].model.editAs; + this.model.path = this.map['v:path'].model; + this.model.fill = this.map['v:fill'].model; + this.model.stroke = this.map['v:stroke'].model; + this.model.imagedata = this.map['v:imagedata'].model; + this.model.shadow = this.map['v:shadow'].model; + + return false; + default: + return true; + } + } + + reconcile(model, options) { + this.map['v:imagedata'].reconcile(model.imagedata, options); + } +} + +VShapeXform.V_SHAPE_ATTRIBUTES = (model, index) => { + if (model.type === VmlShapeType.hfimage) { + return { + id: model.id, + style: + 'position:absolute;left:0pt;top:0pt;margin-left:0pt;margin-top:0pt;' + + `height:${model.style.height};width:${model.style.width};`, + 'o:spid': `_x0000_s${1025 + index}`, + alt: model.imagedata.title, + type: '#_x0000_t75', + 'o:spt': '75', + filled: 'f', + 'o:preferrelative': 't', + stroked: 'f', + coordsize: '21600,21600', + }; + } + + return { + // id + id: `_x0000_s${1025 + index}`, + 'o:spid': `_x0000_s${1025 + index}`, + type: '#_x0000_t202', + style: + 'position:absolute; margin-left:105.3pt;margin-top:10.5pt;width:97.8pt;height:59.1pt;z-index:1;visibility:hidden', + fillcolor: 'infoBackground [80]', + strokecolor: 'none [81]', + 'o:insetmode': model.note && model.note.margins && model.note.margins.insetmode, + }; +}; + +module.exports = VShapeXform; diff --git a/lib/xlsx/xform/drawing/vlm-drawing/vml-stroke-xform.js b/lib/xlsx/xform/drawing/vlm-drawing/vml-stroke-xform.js new file mode 100644 index 000000000..c712e5f71 --- /dev/null +++ b/lib/xlsx/xform/drawing/vlm-drawing/vml-stroke-xform.js @@ -0,0 +1,40 @@ +const BaseXform = require('../../base-xform'); + +class VmlStrokeform extends BaseXform { + get tag() { + return 'v:stroke'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + on: model.on || 'f', + }); + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.reset(); + this.model = { + on: node.attributes.on, + }; + return true; + default: + return true; + } + } + + parseText() {} + + parseClose(name) { + switch (name) { + case this.tag: + return false; + default: + // unprocessed internal nodes + return true; + } + } +} + +module.exports = VmlStrokeform; diff --git a/lib/xlsx/xform/sheet/legacy-drawingHF-xform.js b/lib/xlsx/xform/sheet/legacy-drawingHF-xform.js new file mode 100644 index 000000000..97b97c32b --- /dev/null +++ b/lib/xlsx/xform/sheet/legacy-drawingHF-xform.js @@ -0,0 +1,31 @@ +const BaseXform = require('../base-xform'); + +class LegacyDrawingFHXform extends BaseXform { + get tag() { + return 'legacyDrawingHF'; + } + + render(xmlStream, model) { + if (model) { + xmlStream.leafNode('legacyDrawingHF', {'r:id': model.rId}); + } + } + + parseOpen(node) { + if (node.name === 'legacyDrawingHF') { + this.model = { + rId: node.attributes['r:id'], + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = LegacyDrawingFHXform; diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index f1fd59580..e3df406db 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -30,6 +30,7 @@ const RowBreaksXform = require('./row-breaks-xform'); const HeaderFooterXform = require('./header-footer-xform'); const ConditionalFormattingsXform = require('./cf/conditional-formattings-xform'); const ExtListXform = require('./ext-lst-xform'); +const LegacyDrawingHFXform = require('./legacy-drawingHF-xform'); const mergeRule = (rule, extRule) => { Object.keys(extRule).forEach(key => { @@ -132,6 +133,7 @@ class WorkSheetXform extends BaseXform { tableParts: new ListXform({tag: 'tableParts', count: true, childXform: new TablePartXform()}), conditionalFormatting: new ConditionalFormattingsXform(), extLst: new ExtListXform(), + legacyDrawingHF: new LegacyDrawingHFXform(), }; } @@ -174,10 +176,11 @@ class WorkSheetXform extends BaseXform { Target: `../comments${model.id}.xml`, }; rels.push(comment); + const vmlDrawingIndex = ++options.drawingsCount; const vmlDrawing = { Id: nextRid(rels), Type: RelType.VmlDrawing, - Target: `../drawings/vmlDrawing${model.id}.vml`, + Target: `../drawings/vmlDrawing${vmlDrawingIndex}.vml`, }; rels.push(vmlDrawing); @@ -187,10 +190,37 @@ class WorkSheetXform extends BaseXform { options.commentRefs.push({ commentName: `comments${model.id}`, - vmlDrawing: `vmlDrawing${model.id}`, + vmlDrawing: `vmlDrawing${vmlDrawingIndex}`, }); } + if (model.hfImages && model.hfImages.length > 0) { + const rId = nextRid(rels); + const legacyDrawingHF = (model.legacyDrawingHF = { + rId, + name: `vmlDrawing${++options.vmlDrawingsCount}`, + rels: [], + }); + const drawingHFRel = { + Id: rId, + Type: RelType.VmlDrawing, + Target: `../drawings/${legacyDrawingHF.name}.vml`, + }; + rels.push(drawingHFRel); + options.vmlDrawings.push(legacyDrawingHF); + model.hfImages.forEach(hfImage => { + const rIdImage = nextRid(legacyDrawingHF.rels); + const bookImage = options.media[hfImage.imagedata.index]; + hfImage.imagedata.rId = rIdImage; + hfImage.imagedata.title = bookImage.name; + legacyDrawingHF.rels.push({ + Id: rIdImage, + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', + Target: `../media/${bookImage.name}.${bookImage.extension}`, + }); + }, []); + } + const drawingRelsHash = []; let bookImage; model.media.forEach(medium => { @@ -224,7 +254,9 @@ class WorkSheetXform extends BaseXform { }); } let rIdImage = - this.preImageId === medium.imageId ? drawingRelsHash[medium.imageId] : drawingRelsHash[drawing.rels.length]; + this.preImageId === medium.imageId + ? drawingRelsHash[medium.imageId] + : drawingRelsHash[drawing.rels.length]; if (!rIdImage) { rIdImage = nextRid(drawing.rels); drawingRelsHash[drawing.rels.length] = rIdImage; @@ -348,19 +380,20 @@ class WorkSheetXform extends BaseXform { this.map.headerFooter.render(xmlStream, model.headerFooter); this.map.rowBreaks.render(xmlStream, model.rowBreaks); this.map.drawing.render(xmlStream, model.drawing); // Note: must be after rowBreaks + this.map.legacyDrawingHF.render(xmlStream, model.legacyDrawingHF); this.map.picture.render(xmlStream, model.background); // Note: must be after drawing this.map.tableParts.render(xmlStream, model.tables); this.map.extLst.render(xmlStream, model); - if (model.rels) { - // add a node for each comment - model.rels.forEach(rel => { - if (rel.Type === RelType.VmlDrawing) { - xmlStream.leafNode('legacyDrawing', {'r:id': rel.Id}); - } - }); - } + // if (model.rels) { + // // add a node for each comment + // model.rels.forEach(rel => { + // if (rel.Type === RelType.VmlDrawing) { + // xmlStream.leafNode('legacyDrawing', {'r:id': rel.Id}); + // } + // }); + // } xmlStream.closeNode(); } @@ -415,7 +448,11 @@ class WorkSheetXform extends BaseXform { false, margins: this.map.pageMargins.model, }; - const pageSetup = Object.assign(sheetProperties, this.map.pageSetup.model, this.map.printOptions.model); + const pageSetup = Object.assign( + sheetProperties, + this.map.pageSetup.model, + this.map.printOptions.model + ); const conditionalFormattings = mergeConditionalFormattings( this.map.conditionalFormatting.model, this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'] @@ -433,6 +470,7 @@ class WorkSheetXform extends BaseXform { headerFooter: this.map.headerFooter.model, background: this.map.picture.model, drawing: this.map.drawing.model, + legacyDrawingHF: this.map.legacyDrawingHF.model, tables: this.map.tableParts.model, conditionalFormattings, }; @@ -462,10 +500,14 @@ class WorkSheetXform extends BaseXform { model.comments = options.comments[rel.Target].comments; } if (rel.Type === RelType.VmlDrawing && model.comments && model.comments.length) { - const vmlComment = options.vmlDrawings[rel.Target].comments; - model.comments.forEach((comment, index) => { - comment.note = Object.assign({}, comment.note, vmlComment[index]); - }); + const match = rel.Target.match(/\/drawings\/([a-zA-Z0-9]+)[.][a-zA-Z]{3,4}$/); + if (match) { + const name = match[1]; + const vmlComment = options.vmlDrawings[name].comments; + model.comments.forEach((comment, index) => { + comment.note = Object.assign({}, comment.note, vmlComment[index]); + }); + } } return h; }, {}); @@ -514,6 +556,22 @@ class WorkSheetXform extends BaseXform { } } + model.hfImages = []; + if (model.legacyDrawingHF) { + const legacyDrawingHFRel = rels[model.legacyDrawingHF.rId]; + const match = legacyDrawingHFRel.Target.match(/\/drawings\/([a-zA-Z0-9]+)[.][a-zA-Z]{3,4}$/); + if (match) { + const vmlDrawingName = match[1]; + + const {hfImages} = options.vmlDrawings[vmlDrawingName]; + hfImages.forEach(hfimage => { + if (hfimage) { + model.hfImages.push(hfimage); + } + }); + } + } + const backgroundRel = model.background && rels[model.background.rId]; if (backgroundRel) { const target = backgroundRel.Target.split('/media/')[1]; diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index ab62797ec..e6d34fe26 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -23,7 +23,7 @@ const PivotCacheRecordsXform = require('./xform/pivot-table/pivot-cache-records- const PivotCacheDefinitionXform = require('./xform/pivot-table/pivot-cache-definition-xform'); const PivotTableXform = require('./xform/pivot-table/pivot-table-xform'); const CommentsXform = require('./xform/comment/comments-xform'); -const VmlNotesXform = require('./xform/comment/vml-notes-xform'); +const VmlDrawingXform = require('./xform/drawing/vlm-drawing/vml-drawing-xform'); const theme1Xml = require('./xml/theme1'); @@ -84,6 +84,7 @@ class XLSX { const worksheetXform = new WorksheetXform(options); const drawingXform = new DrawingXform(); const tableXform = new TableXform(); + const vmlDrawingXfrom = new VmlDrawingXform(); workbookXform.reconcile(model); @@ -111,6 +112,18 @@ class XLSX { } }); + Object.keys(model.vmlDrawings).forEach(name => { + const vmlDrawing = model.vmlDrawings[name]; + const vmlDrawingRel = model.vmlDrawingRels[name]; + if (vmlDrawingRel) { + drawingOptions.rels = vmlDrawingRel.reduce((o, rel) => { + o[rel.Id] = rel; + return o; + }, {}); + vmlDrawingXfrom.reconcile(vmlDrawing, drawingOptions); + } + }); + // reconcile tables with the default styles const tableOptions = { styles: model.styles, @@ -147,6 +160,7 @@ class XLSX { delete model.drawings; delete model.drawingRels; delete model.vmlDrawings; + delete model.vmlDrawingRels; } async _processWorksheetEntry(stream, model, sheetNo, options, path) { @@ -216,9 +230,15 @@ class XLSX { } async _processVmlDrawingEntry(entry, model, name) { - const xform = new VmlNotesXform(); + const xform = new VmlDrawingXform(); const vmlDrawing = await xform.parseStream(entry); - model.vmlDrawings[`../drawings/${name}.vml`] = vmlDrawing; + model.vmlDrawings[name] = vmlDrawing; + } + + async _processVmlDrawingRelsEntry(entry, model, name) { + const xform = new RelationshipsXform(); + const relationships = await xform.parseStream(entry); + model.vmlDrawingRels[name] = relationships; } async _processThemeEntry(entry, model, name) { @@ -277,6 +297,7 @@ class XLSX { comments: {}, tables: {}, vmlDrawings: {}, + vmlDrawingRels: {}, }; const zip = await JSZip.loadAsync(buffer); @@ -406,6 +427,11 @@ class XLSX { await this._processVmlDrawingEntry(stream, model, match[1]); break; } + match = entryName.match(/xl\/drawings\/_rels\/([a-zA-Z0-9]+)[.]vml[.]rels/); + if (match) { + await this._processVmlDrawingRelsEntry(stream, model, match[1]); + break; + } } } } @@ -461,6 +487,23 @@ class XLSX { }); } + addlegacyDrawingHF(zip, model) { + const vmlDrawingXform = new VmlDrawingXform(); + const relsXform = new RelationshipsXform(); + + model.worksheets.forEach(worksheet => { + const {legacyDrawingHF} = worksheet; + if (legacyDrawingHF) { + vmlDrawingXform.prepare(legacyDrawingHF, {}); + let xml = vmlDrawingXform.toXml({hfImages: worksheet.hfImages}); + zip.append(xml, {name: `xl/drawings/${legacyDrawingHF.name}.vml`}); + + xml = relsXform.toXml(legacyDrawingHF.rels); + zip.append(xml, {name: `xl/drawings/_rels/${legacyDrawingHF.name}.vml.rels`}); + } + }); + } + addTables(zip, model) { const tableXform = new TableXform(); @@ -633,7 +676,7 @@ class XLSX { const worksheetXform = new WorksheetXform(); const relationshipsXform = new RelationshipsXform(); const commentsXform = new CommentsXform(); - const vmlNotesXform = new VmlNotesXform(); + const vmlDrawingXform = new VmlDrawingXform(); // write sheets model.worksheets.forEach(worksheet => { @@ -653,7 +696,7 @@ class XLSX { zip.append(xmlStream.xml, {name: `xl/comments${worksheet.id}.xml`}); xmlStream = new XmlStream(); - vmlNotesXform.render(xmlStream, worksheet); + vmlDrawingXform.render(xmlStream, worksheet); zip.append(xmlStream.xml, {name: `xl/drawings/vmlDrawing${worksheet.id}.vml`}); } }); @@ -676,7 +719,8 @@ class XLSX { model.created = model.created || new Date(); model.modified = model.modified || new Date(); - model.useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : true; + model.useSharedStrings = + options.useSharedStrings !== undefined ? options.useSharedStrings : true; model.useStyles = options.useStyles !== undefined ? options.useStyles : true; // Manage the shared strings @@ -696,9 +740,11 @@ class XLSX { styles: model.styles, date1904: model.properties.date1904, drawingsCount: 0, + vmlDrawingsCount: 0, media: model.media, }; worksheetOptions.drawings = model.drawings = []; + worksheetOptions.vmlDrawings = model.vmlDrawings = []; worksheetOptions.commentRefs = model.commentRefs = []; let tableCount = 0; model.tables = []; @@ -732,6 +778,7 @@ class XLSX { await this.addWorksheets(zip, model); await this.addSharedStrings(zip, model); // always after worksheets await this.addDrawings(zip, model); + await this.addlegacyDrawingHF(zip, model); await this.addTables(zip, model); await this.addPivotTables(zip, model); await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]); diff --git a/spec/unit/xlsx/xform/drawing/vml-drawing/vml-drawing-xform.spec.js b/spec/unit/xlsx/xform/drawing/vml-drawing/vml-drawing-xform.spec.js new file mode 100644 index 000000000..62c15166f --- /dev/null +++ b/spec/unit/xlsx/xform/drawing/vml-drawing/vml-drawing-xform.spec.js @@ -0,0 +1,153 @@ +const testXformHelper = require('../../test-xform-helper'); + +const VmlDrawingXform = verquire('xlsx/xform/drawing/vlm-drawing/vml-drawing-xform'); + +const options = { + rels: { + rId1: {Target: '../media/image1.jpg'}, + rId2: {Target: '../media/image2.jpg'}, + }, + mediaIndex: {image1: 0, 'image1.jpg': 0, image2: 1, 'image2.jpg': 1}, + media: [{}, {}], +}; + +const expectations = [ + { + title: 'hfimage', + create() { + return new VmlDrawingXform(); + }, + preparedModel: { + comments: [], + hfImages: [ + { + anchor: null, + editAs: null, + fill: { + focussize: '0,0', + on: 'f', + }, + id: 'RH', + imagedata: { + index: 0, + rId: 'rId1', + title: 'pig (1)', + }, + path: {}, + protection: null, + shadow: null, + stroke: { + on: 'f', + }, + style: { + height: '142pt', + left: '0pt', + 'margin-left': '0pt', + 'margin-top': '0pt', + position: 'absolute', + top: '0pt', + width: '142pt', + }, + type: 'hfimage', + }, + ], + }, + xml: ` + + + + + + + + + + + + + + + + `, + parsedModel: { + comments: [], + hfImages: [ + { + anchor: null, + editAs: null, + fill: { + focussize: '0,0', + on: 'f', + }, + id: 'RH', + imagedata: { + rId: 'rId1', + title: 'pig (1)', + }, + path: {}, + protection: null, + shadow: null, + stroke: { + on: 'f', + }, + style: { + height: '142pt', + left: '0pt', + 'margin-left': '0pt', + 'margin-top': '0pt', + position: 'absolute', + top: '0pt', + width: '142pt', + }, + type: 'hfimage', + }, + ], + }, + reconciledModel: { + comments: [], + hfImages: [ + { + anchor: null, + editAs: null, + fill: { + focussize: '0,0', + on: 'f', + }, + id: 'RH', + imagedata: { + rId: 'rId1', + title: 'pig (1)', + }, + path: {}, + protection: null, + shadow: null, + stroke: { + on: 'f', + }, + style: { + height: '142pt', + left: '0pt', + 'margin-left': '0pt', + 'margin-top': '0pt', + position: 'absolute', + top: '0pt', + width: '142pt', + }, + type: 'hfimage', + }, + ], + }, + options, + tests: ['render', 'renderIn', 'parse', 'reconcile'], + }, +]; + +describe('VmlDrawingXform', () => { + testXformHelper(expectations); +}); 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 9e3a0ef59..8348d7b7e 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.0.json @@ -71,6 +71,19 @@ "evenHeader": "&C&KCCCCCC&\"Aril\"3 exceljs evenHeader", "evenFooter": "&Lexceljs&C&F&RPage &P evenHeader" }, + "hfImages": [ + { + "id": "HF", + "type": "hfimage", + "style": { + "width": "55pt", + "height": "55pt" + }, + "imagedata": { + "index":0 + } + } + ], "media": [], "rowBreaks": [], "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 dd989aefc..78c094604 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json @@ -74,6 +74,11 @@ "Target": "https://www.npmjs.com/package/exceljs", "TargetMode": "External", "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + }, + { + "Id": "rId2", + "Target": "../drawings/vmlDrawing1.vml", + "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" } ], "comments": [], @@ -90,5 +95,33 @@ "evenHeader": "&C&KCCCCCC&\"Aril\"3 exceljs evenHeader", "evenFooter": "&Lexceljs&C&F&RPage &P evenHeader" }, + "hfImages": [ + { + "id": "HF", + "type": "hfimage", + "style": { + "width": "55pt", + "height": "55pt" + }, + "imagedata": { + "index": 0, + "rId": "rId1", + "title": "image1" + } + } + ], + "legacyDrawingHF": { + "name": "vmlDrawing1", + "rId": "rId2", + "rels": [ + { + "Id": "rId1", + "Target": "../media/image1.jpg", + "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + } + ] + }, "conditionalFormattings": [] } + + diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.1.2.xml b/spec/unit/xlsx/xform/sheet/data/sheet.1.2.xml index b286db960..41761d297 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.1.2.xml +++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.2.xml @@ -71,5 +71,6 @@ &Lexceljs&C&F&RPage &P evenHeader Hello Exceljs Hello World - + + \ No newline at end of file diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.1.3.json b/spec/unit/xlsx/xform/sheet/data/sheet.1.3.json index cb4981573..0f5d1abfa 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.1.3.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.3.json @@ -85,6 +85,9 @@ "evenHeader": "&C&KCCCCCC&\"Aril\"3 exceljs evenHeader", "evenFooter": "&Lexceljs&C&F&RPage &P evenHeader" }, + "legacyDrawingHF": { + "rId": "rId2" + }, "tables": null, "conditionalFormattings": [] } diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.5.3.json b/spec/unit/xlsx/xform/sheet/data/sheet.5.3.json index 08627ab47..0d2fde09e 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.5.3.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.5.3.json @@ -15,6 +15,7 @@ } ], "background": null, + "legacyDrawingHF": null, "mergeCells": null, "dataValidations": null, "drawing": null, diff --git a/spec/unit/xlsx/xform/sheet/data/sheet.6.3.json b/spec/unit/xlsx/xform/sheet/data/sheet.6.3.json index 4d7984b52..6a0417886 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.6.3.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.6.3.json @@ -17,6 +17,7 @@ ], "background": null, "mergeCells": null, + "legacyDrawingHF": null, "dataValidations": null, "drawing": null, "headerFooter": null, diff --git a/spec/unit/xlsx/xform/sheet/worksheet-xform.spec.js b/spec/unit/xlsx/xform/sheet/worksheet-xform.spec.js index 8c571a0fc..fd29e12e6 100644 --- a/spec/unit/xlsx/xform/sheet/worksheet-xform.spec.js +++ b/spec/unit/xlsx/xform/sheet/worksheet-xform.spec.js @@ -66,6 +66,12 @@ const expectations = [ styles: fakeStyles, formulae: {}, siFormulae: 0, + vmlDrawings: [], + vmlDrawingsCount: 0, + media: [ + {name: 'image1', extension: 'jpg'}, + {name: 'image0', extension: 'jpg'}, + ], }, }, { From 41787b0564e16347665fad095ebb73b9385e7f28 Mon Sep 17 00:00:00 2001 From: zhongnan chi Date: Mon, 30 Oct 2023 11:55:54 +0800 Subject: [PATCH 2/6] fix: api error --- lib/doc/worksheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 9eff826cc..a9c4445f0 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -759,7 +759,7 @@ class Worksheet { return image && image.imageId; } - addFHImage(imageId, options) { + addHFImage(imageId, options) { const model = { type: 'hfimage', id: options.id, From 0a222eb28011781b2c11fb606f512c6c6065fb27 Mon Sep 17 00:00:00 2001 From: zhongnan chi Date: Tue, 28 Nov 2023 18:43:29 +0800 Subject: [PATCH 3/6] fix: repeate legacyDrawing --- lib/xlsx/xform/sheet/worksheet-xform.js | 21 ++++++++++--------- .../unit/xlsx/xform/sheet/data/sheet.1.1.json | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index e3df406db..770d9a71f 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -205,6 +205,7 @@ class WorkSheetXform extends BaseXform { Id: rId, Type: RelType.VmlDrawing, Target: `../drawings/${legacyDrawingHF.name}.vml`, + isHFRel: true, }; rels.push(drawingHFRel); options.vmlDrawings.push(legacyDrawingHF); @@ -386,14 +387,14 @@ class WorkSheetXform extends BaseXform { this.map.extLst.render(xmlStream, model); - // if (model.rels) { - // // add a node for each comment - // model.rels.forEach(rel => { - // if (rel.Type === RelType.VmlDrawing) { - // xmlStream.leafNode('legacyDrawing', {'r:id': rel.Id}); - // } - // }); - // } + if (model.rels) { + // add a node for each comment + model.rels.forEach(rel => { + if (rel.Type === RelType.VmlDrawing && !rel.isHFRel) { + xmlStream.leafNode('legacyDrawing', {'r:id': rel.Id}); + } + }); + } xmlStream.closeNode(); } @@ -451,11 +452,11 @@ class WorkSheetXform extends BaseXform { const pageSetup = Object.assign( sheetProperties, this.map.pageSetup.model, - this.map.printOptions.model + this.map.printOptions.model, ); const conditionalFormattings = mergeConditionalFormattings( this.map.conditionalFormatting.model, - this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'] + this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'], ); this.model = { dimensions: this.map.dimension.model, 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 78c094604..eda97a102 100644 --- a/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json +++ b/spec/unit/xlsx/xform/sheet/data/sheet.1.1.json @@ -78,7 +78,8 @@ { "Id": "rId2", "Target": "../drawings/vmlDrawing1.vml", - "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", + "isHFRel": true } ], "comments": [], From d5d97e9861ccfe0db1693cccc948a134248582fc Mon Sep 17 00:00:00 2001 From: baian1 Date: Sat, 28 Dec 2024 22:48:21 +0800 Subject: [PATCH 4/6] fix: remove isHFRel attribute and fix repeat vmlDrawing node --- lib/xlsx/xform/core/content-types-xform.js | 2 +- lib/xlsx/xform/core/relationship-xform.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xlsx/xform/core/content-types-xform.js b/lib/xlsx/xform/core/content-types-xform.js index 065feb617..05945d163 100644 --- a/lib/xlsx/xform/core/content-types-xform.js +++ b/lib/xlsx/xform/core/content-types-xform.js @@ -105,7 +105,7 @@ class ContentTypesXform extends BaseXform { }); } - if (model.vmlDrawings && model.vmlDrawings.length > 0) { + if (model.vmlDrawings && model.vmlDrawings.length > 0 && !model.commentRefs) { xmlStream.leafNode('Default', { Extension: 'vml', ContentType: 'application/vnd.openxmlformats-officedocument.vmlDrawing', diff --git a/lib/xlsx/xform/core/relationship-xform.js b/lib/xlsx/xform/core/relationship-xform.js index da6831939..01603f2a7 100644 --- a/lib/xlsx/xform/core/relationship-xform.js +++ b/lib/xlsx/xform/core/relationship-xform.js @@ -2,7 +2,7 @@ const BaseXform = require('../base-xform'); class RelationshipXform extends BaseXform { render(xmlStream, model) { - xmlStream.leafNode('Relationship', model); + xmlStream.leafNode('Relationship', {...model, isHFRel:undefined}); } parseOpen(node) { From 264d2ed24d05de599a85f322256b3e57ba32f2d4 Mon Sep 17 00:00:00 2001 From: chizhongnan Date: Sat, 11 Jan 2025 17:02:18 +0800 Subject: [PATCH 5/6] fix: comment and hgimage use vml error --- lib/xlsx/xform/sheet/worksheet-xform.js | 6 +++--- lib/xlsx/xlsx.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 770d9a71f..660a887f8 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -176,7 +176,7 @@ class WorkSheetXform extends BaseXform { Target: `../comments${model.id}.xml`, }; rels.push(comment); - const vmlDrawingIndex = ++options.drawingsCount; + const vmlDrawingIndex = ++options.vmlDrawingsCount; const vmlDrawing = { Id: nextRid(rels), Type: RelType.VmlDrawing, @@ -452,11 +452,11 @@ class WorkSheetXform extends BaseXform { const pageSetup = Object.assign( sheetProperties, this.map.pageSetup.model, - this.map.printOptions.model, + this.map.printOptions.model ); const conditionalFormattings = mergeConditionalFormattings( this.map.conditionalFormatting.model, - this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'], + this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'] ); this.model = { dimensions: this.map.dimension.model, diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index e6d34fe26..260ffd944 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -696,7 +696,7 @@ class XLSX { zip.append(xmlStream.xml, {name: `xl/comments${worksheet.id}.xml`}); xmlStream = new XmlStream(); - vmlDrawingXform.render(xmlStream, worksheet); + vmlDrawingXform.render(xmlStream, {...worksheet, hfImages: undefined}); zip.append(xmlStream.xml, {name: `xl/drawings/vmlDrawing${worksheet.id}.vml`}); } }); From c22c7dd805bd6719068b7663708693237018767d Mon Sep 17 00:00:00 2001 From: baian1 Date: Sat, 11 Jan 2025 22:58:54 +0800 Subject: [PATCH 6/6] fix: legacyDrawing elemets sortIndex --- lib/xlsx/xform/sheet/worksheet-xform.js | 2 +- .../data/comments-headerImage-sheetimage.xlsx | Bin 0 -> 26461 bytes .../data/comments-headerImage.xlsx | Bin 0 -> 23483 bytes spec/integration/pr/test-pr-2563.spec.js | 19 ++++++++++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 spec/integration/data/comments-headerImage-sheetimage.xlsx create mode 100644 spec/integration/data/comments-headerImage.xlsx create mode 100644 spec/integration/pr/test-pr-2563.spec.js diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 660a887f8..47200284e 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -381,7 +381,6 @@ class WorkSheetXform extends BaseXform { this.map.headerFooter.render(xmlStream, model.headerFooter); this.map.rowBreaks.render(xmlStream, model.rowBreaks); this.map.drawing.render(xmlStream, model.drawing); // Note: must be after rowBreaks - this.map.legacyDrawingHF.render(xmlStream, model.legacyDrawingHF); this.map.picture.render(xmlStream, model.background); // Note: must be after drawing this.map.tableParts.render(xmlStream, model.tables); @@ -395,6 +394,7 @@ class WorkSheetXform extends BaseXform { } }); } + this.map.legacyDrawingHF.render(xmlStream, model.legacyDrawingHF); xmlStream.closeNode(); } diff --git a/spec/integration/data/comments-headerImage-sheetimage.xlsx b/spec/integration/data/comments-headerImage-sheetimage.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5ae791a06f7b5fb5f3d7f4b3e26f187d25f7f859 GIT binary patch literal 26461 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 b#~+UpMOk#DQpWkSh6s?2WX7f%o*(@O6uMeC literal 0 HcmV?d00001 diff --git a/spec/integration/data/comments-headerImage.xlsx b/spec/integration/data/comments-headerImage.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d8fc2b78721f8c27381b8a91ed8a7620dfb93c85 GIT binary patch literal 23483 zcmeFX1y^KElP+3~yEd*3jk~+MH*O6yP636xHqf{=?(XjH?(XjH?r?d(Z_c@M?##M> zU{0>uu_bd==FW^KBK8ycLk1k;D*y@r0{{TN0q}HK`ij2*0M1YV06G8$Ok2d-%E8#m zK~Kfi#@Jqm-UVbyoDBg+nF#>RjAC zDJw*Ma$6O3E`Nk_Wj5lZr6cGapjK{LM%I}HZ`Z{ZYTLk-pmfLNfTC_yW17UdF(gatS=s$~#hJg~gK=IpMyDXzYso)np98Jw1?^$PFJrg(oh#*Qq5 zLcc9AJcKg@Gp>XrUQlJyG?8Tx?Dfz3OT#5V!ulaub|QArd7KJCa8BVc6&%81&$PH4 zWV%|;R-BK|m5ZM0oiK~F&=!zcpsP@ca^GwET@$>oM*6l8A6ayq`w3%um+y~M9gfk4 z>t2(pyk>{xDAao^j@s@A4LV%4_v~#&Fgh|WsNudc<|f(Hfw1p+E*UZdgro$glAhvXlio1 zcyZg(tzJ~8xx2ahBnfF(N~g9+>XOF7ESaGVQt_E9(JGV)dUae_^gR3kOui%^t$sPp zb%V$9FLT1mXJrAE4IG)rF%v1?3-S3!hyp=evZvEY7=!i(rc31>gO=a!o^Vx_&A5P- z2I+R(B(8c!mYp}EDIMr<9(3}lgGwZdb7EyDv z9bx0NFuS4XIFp@AkI}c}F>KGKr^AT9Ifx)Qt)YO(Dbj_*p4~`iES{qCi`-UcK<1R8 z3hsu>+(-eu7DSE=o3Ph#ZodY@CoHPVJ^Hv`7$~D&NHs zK-9(f*dxEmy;O`TD@C`kkDuAbcNFsodk^Cp=X=fyZ7l-F*GKfHY%3}*#5+pWqcaqV}>A!8`Bq)r6 z6c%fIE6-3wl~z{VZx&ysMb=?&avS0troM3}-H=)a@prjU_rAmzx7jUA+>QF}BU577 zlABkuD}Dd{>A(S?9fz2#jl0)*v-b*j?IlN)3HRe<+eVV%+t~L;P~!05nR5E5Au~lV z9;DEAz&o$Q4EBH(Kbb54Kn-vI6f%r>A5M&e#juDE-7U8Nk1_NGy;?k82vcoE-Wiwf zz(|XOjDW6(9wuVov`;LTH{aa7O30hZIU#SZ^tbsjD># zdUmoO88chWp_FXz*V0h)2gMoXd(g5$T3AEq24rL zs(7P|#E^CA;{KghfOKgo!_>%OtF7}(T8!F0?7i|xu=BojiWgKkyd(^) zZTcpw!xc%;s!iGiy4Rwe>yDKrq7Bdqk z=lT|gwJPM>SuIkR_}tVi#d=FQx<~CRkGoDBNTBqRrl0SFq(_3DuBBdO%r~#>e8*mr z&~IHSD66D^BB;9DUT)SZWz7oXpvi^JT<$?!{g`~`%>#U1J)3Gzs>Y3kg;b2T(BXq7 zkZK)VMycC^SvM+d=A^SNKuuE55XGZc>UMCe@zq4`31z`I<{6;xQlm^*&Bo`U<(&z| zB+LIU4Iy%no3!FRdZ^|qQx6whjoYu>i*EGrH*ZA5FVwciWSWTSZB_xaZ8FEmI&Jub z>oNMwi+*!mfuqsO_wo6C@?WiX9ohmL*QZ9QeAZ(BzgFAc%-Gn$p5b37rhm+LYK(Sd z3L}dD2F;y-h8M+8;Xx?>(qy#RxMK6U)B80h5d%m=bDjpZw+n)p0J*pXGqS&7h#RRa zqe=XP#&Y+W;XmZWdxQjGS%1c5YntBP;q{7Fa9G}eUBUkxbir~I0758x0+h*1a$A~ z427W!ChDa1b1md9x@7HOtwm+i1hk=<0>>Oy?YT#Z2>nq`F7sBB)SL{n_-9d@Rlz-)N2$N`weQBxW$y!BBTf zBt`JUU*u55YRV8>)Q_whAeGI1Dou9Q@L>UET4bR>ic`#+NN3m2vS8y9ve{0$U*UhF z`P%$7V0QUL>q!rhnX-A%2wQj zy?$TIs*epBu7>b=1mFV2e1+bZVEf66@7$XfPC{?9N6u=<$y6-WS1yh(A$#2nt8Bvf z>_rmd3-A#J@C~-QP_n%L<=8*9;Nzp3YFtu|&Q^+6sDiK2e#7_SIik+Y%91MjeNyq$wmeAiJI*3`VaHsmC z4B785q~ML0`>t3Aitj8(ecOH;W655PQw_m z=N>y-Kw>>S3TxfU-AO%1n;=-gK9F+%GYtB#aJ&CiE7bT|gG%xbS@5qQ$-&GRWX$j{ z`7flMYK=wWHDGi=4}J)a>dcQGy6baI7I563yKTkk>`F?-2I!942r9MEeEkAsH3_+x%%cc^tZV#_~6C_b@)}%GI4LLEE~;jiMsd$^i?6(ze}X2rv35f`@Z&HZRt+5y&v~ zQWm+M@W@4mwellB0+5~bVuwCf)(2GYP+50ouVN4tC}m3}Yk ziEpAz+ItqP58gbS8F!89Yc zqL#)B>6zn)hZXT^W<;pnmt^VazUdyvza|fgjBk8z{a~txzu7|0gIf+1w<`qwVU}88 zYoWRhlBO}57>vHyvTtpVcbVr5u39X3f@rn!KUe8AaQNZUYjR+`3G082yxj+kl|XZd z^>wdRI061CYm?cKac(X5-|OJeG_0i0SaZkw0`2l7UVO(edhPo`I}(*ND*}(^kV!fv;O)cfaN3 zUpIzQ5v>fiGGw*gOb4=(!MpfquU%@0w}ww%@!hY_v~BbC{Og-vsGr$NrNx(jg?@4N zLQ83AYCbj?#ac!LTfuh2gpiyb3zW5b`3;0fcy|2-2H)^?agexc&blyUoFrA)o03sy zG;dUuflRvTPkj^)`@QY;;b!J39zca78uPnwbxyzd@N_?;t_eB$)iBiC(%!UDU(RAm zOj`#eKkHa#WipR6lD?d)Z%Ea+Tt`wn1Q{^R#WOl)$FQ436Q{mx)uIq`y z4u#RNhOK&B*Q^KRaI2H9m@0(K5#L09**XwZ5VM^eZ1=$K0itLhYzQP?rCAeikNGU5 z;Y60KdhGmQTj$sigxGqwChm+9q``;0tHeU0_S2s%o$b-DnYNm9ixXPJ58zv@P1#vL zAZ%vb%oML4PaV(X#-ebBS}h$e7LoMzyEu|0Rc$Qxh#24HH%r~Nf_k?PUpoL40Qf@Xh zJ>4u70oxt;=9;zhEYYNX93|Hi^aBk;>2P{@SNsqgQgAl*mmSS6PgFALkH$Oc z?CZ9=J_K4qyJ*T7S!x+ShaXMLB#ac{18)lx4;$@x6=3$mPT_ML0a~Z?;mVY4OxywD zBKhzv=h;7^VnTMAaNjV^BGjXImTmo2Vaw(idB5l+PSSmRz@Qx06d_;Qig%`<`Qj+M&&3ym&!c&q6m zrT13EP?|&>4$)uLp@~_KZsD@!uU1ctWOnLy-q%DNp9hX*$lMszeydSQ7j1lfb$>E; z-o0nI+khpYC<-Kr>@m?@yDFe+PzXmFop_s^NXNNi=Gp=Oec<`Xr2U!0rkfLEFR42#{vRh? zw|7fyS%x0gQ(I&rE*_nMn`A1>!9a95E`62Lx@DT#24Bg_HyN~XUD$$`Kv}QdL3^QDQ1|6$qPc`6HBR zS0L0B=8@Gi28BVFHQ_P3mk$Tk(t0LUCqNdY?AryThyFCO z(|7*og&3SbmZJZ@W}^SR4pZ5%{)E4m#I|?ksED3cG6XwF7Y>hWHl{4E=|k!wIh< zf+3_rv$@0Tfb)!-^;>cM)!VbjhV#O{h#`(=Dwb!La~gVku_NhQHj^qe;e2CZCaS05 z(+Y<&`6~_jyoE9lm1u&k$9wiLYdSu`K>?|Ip?Xg3TtBcB%dOMb!q^5Ouu(DGQcpB7 zni4+!dJm?`!$)!7^G~m5Ns=OD2OgT+ntr@dswyDudS4NXS7su{h~LXz9Gh+p->#5u zUb+;1eH0Yt%(T!ykXf*QNPhaOWQh;^z}6bJj;XL^5hq6t3}wYZ3Fpiv7jDJZu(1;C zz`;Lj!q?e>`;zOMjgkFuZ-JCkzb386gRico!MrbrSto4M^)Khb3a}z^C%hd@wF<>l zopu3>D|5SVwcF97&^eWuz~qQ7trWTNZC;$R0e@R*WD4fs>020`Hjk{?jTm5S(g)?d@O~UquZv%UO zNZ8ick_>Xu+>nV4w0bOs!m=;>HCE|AX8IE<$0KjU-##aFJ&S9f zkZx_h3$;9lyezA-IaD#lk8386;FkaOrRJk;vN1K2b&Y5P+X&H- z(k1OtHny^4MZV)BjpJ$M_Qy$GDjcBTj*)ge`lVtfie`DIUc>%(3W-S4)W-U$kZ=FL z3d#I$h3xvrAGLkvCF8kH{iky+8#Y13gq*Z!6!h*tlX`)&pvbB;_7=T~MfofpvpH!; z=JX_vi5(72BrVieya^|avo!to^FRAu*~Vv`B6#F%X7w{6?Vc>+-FcE`+M4k3MBerR zGE&lvlS$)V^`)Nbc= z;+$W1s#uA#T4bqU5m*b(3@w^0=wJ1SQeeHCwZ`I?OP@Wto7A*e9BS~^5$R~~?kn3-m?FE5h% z?tVMFu@SCOko{EWI_2hAo;)W^6+9>5VqZ*sZJ?5QC0RB1t!={6bjezRWcMOs`+9Ps7SF?{)lp#E3^ z`~kp1L8GIiVW6R*Vq#)oVPaunVPj%{7A#ysY-~I{3_M(HB1&RBA~HN2EK;U#WK=Zt z^z_(7ESxNK9F%nQG+*H0;E)iJ@K8|j7|4mqndunl=-B9J{yBaA=olD3#lOlwcmI0y z@7w<^KRN-ZFo3o%@Zeui0bru zsIr0K6_&l{w9OXh>MWj;R=E$>Ujk8>rs!RKd104N{v+ zKjh!npPO$*x9^*JtMr10)=J(EznW^WTa(x6XOna3M7|(?57oV>o;#>jK4LFJbo&6* zzoLe!o4KGqNuFonGsUNl8GPMGyZ!PLQA`WvS?>gv%&dT)ALT=Hnp^wdS1aUP~3Cedf)#b zI`L%3^qyzb!M)7o)w;7vo*w!A3NKC}YYi=ZM)?nK_hh})%iV&9r#w`t-&2?LhcxJ>Y=-7B%z}uaJ!Dl-!>(~8I`v*YO zb+!k%Ho$k+AfhEJhw~ejzlRGUA-REb#7n$SS@?vq8)xFBy>#t#Kx(ccz`aC;!KPUJ zCM<2bABC5N#<6sdlg~u`=tWEIftocQlf!W@2J(9j3>e@yF$_|9A(RnP9crhn{wX3C4R;+Dx81WHpydVA_{`;e2%Eu|^j*<`1NVgVC2;HOwSgxIK0HqZ zb&w#M7%yqs^Y*rVjr70@76j;B+g3A8rG-x(uNFZ$r)Fx`?*qUzb=Y@MCwCt74hh0P zQP(9bU~{aH=8Td7C-q@1cHirAJDGRSc16U=$?i~U2)Q*?2g-47(eU6fUlAQX0J*CstT_b%*8d-D;X<&qyBGScCEV-_(&>`LNn2R4K#$oYtmzeV@pp(GyV6R%Ldd;s}W;;oKriYk6r%cazX%oTX98 z`k{i9uZ1rngk|v)#*bndA1k`?kJ0w%p_xVUZH^jtZ%ud`)PfkWm7l~Ncgz*1fdZ+F zgD}*HTHG7{+fU7fhWApf>kD=oW~QA`qpF>hpL`?z^*A#w$)09DmX_~OEiTqoJ!4U? z)C39L<1w)><}(S(Iuqv_=D<;iFJFG1oX7UF;7JD%Hsvpe<>S&qy`=B2cG*A;BlaFs zhcLO17l~!Rk@)pmH_*7!)+GDIgrI%45qtW3+_oDp2PC>R$$j6OWR<$A$8Zng(jyfPrYnlSaou$#8D?igQG9e9KZB(ZtH zNjbemYPnPQgq_UsK{CCP{j;8UH#^2#G@L5RTHDqUWptu{9wz)j|29Ot&Y9KoLg-G3p$t9X5pLV zqoi_$HPAS_<6E$J%mBMTZd@uXh@ESfgBg$rCz@A+ANQ5AdCpMI+8P3`d{$P5F+TA^ z74Qrdhq!(^EFp&^d)8b0AaSmrR-{IJzI9aJO*ZbF_Pf^hXiWdCB6H)lLXIst{GLHs zzqt8D8#UONdNjA67lp%!-~&)%w?iCioTNF`9J?1FI?W9i*Va-bSHF;e)}tKp%MZjM zy=~@1BvA^yxKEh}ZL)?o65%%$CMc)c7LNxCF0U-uALNvU8G27v=#*hk3p&x4+qm~- z-;Y&h-rTv#7d)y)YpN_{B<)DxgqB{(HHMtqk7z9wzeEfAk&j?-N4vZ}w5%>U z(=+X8o{u|4k+cigma2s38WS^)Iy;na6O&IKF{Ubg9trAGAv>olxTC5a5oEF_oC##v z1}iSpQg(MH5}aCQ_Alj|OR}~0HdX9c3aFSVy$LR|ml|8OFao?jgt=I)P{q|xN5{;2 zz>WXdn9XfVCu#zD(qyik(Wp};GxS$l&6^zAw(?cacM*cobI~oR#DpfVhn#`uQj3>@ z^6HVDm5-^1Rew$s4q&$B_f;<-iAMFmdr3F3*XPv5eiN{>VDc<%V1w6=`0m6uh>=Wd z$s>ft$ZXSkGp4z+u%Hot<3EE-vy?Zk^hgxAn1n%@O%opDY`%T8-dE3$R7kgllDTwh zC!stn;P%yY?pEw?0e+<*3P%4l)Hx5N5`s0etFw9S#=-?P;=&#A2Y@3@-tVFGoOJdB z@LX+J`mC{Kb5Tx}5Z@Wv8H3l)2}xagM_Q_=v0RdT923v1KF6LELL5Jp4TBytp;yZM zLudDqv>=lFY({Iwy2%q~OWTa|5Gnav!Hp*Upbb6yduu|(d4XazF4zR;L(R?7uC*h& zhOI_L9($h70egxZr3D_u#j4;N{x048T>+ob@ETLj*_msBJg3~*@`eyWQE9W1Pe`52 zPvWLJJbB3v0E5#`FC9{y8DD8_MEFz#JnOtVRLa$|q@y>G&fSkm79lYtEN$i8UN-)+ zG0MGrL;j2}nufuZhm(RDnNPvnaA#8k!G;*c2+x<98aT!})5F@lmMPC1XhfZC#LJ5v zA5A`vFLqD}uWIhGYa8tp-li7UG(Z4K`~dhS_8-njw@D*|y9PQ*1*9C91tdKdCkge- zQ+SK-sb=9$t-gE!cDqkIYm=aMo=a_{=dCShbrL2)S{ls^(aSzrtF&8+`J`cx+*8I< zEYW)iYuvR31Sg!QJv_j+L=MOCZdIpy_c(gw-;O!s>NPcbm0EVmTEiTuat;;3?R`p) z%pyI0iDwIJlN^$*JQHu{nYBBu$5i7OY6kPT!=3`9hvuS~ew{uSaA!pH@hVAR%U%^M zmCth2`uS<;*pnQIAadxrx+jHLnPM4rO#&i{i`|kNHbRvKQ@@v|xIAeR@UoUo<0T~g zjwsc|ZO5yWk&t!9zOK%?exdj%oQNCA42!lU-IEt=Eo6B!0KZ>chF6XpB%Z&()zWichtt(%jN6r-}ceKys*NWhi=BpE25CDy%CkHM zRWy<8MI;!lhXDlYNb%=3;1VBy^B*Nxu;=j;;~hobW~KHPQw5q$d|5r+;=1C)$7u5* zQ8zRp%sFZ*du{ANm=kzSxwfMjr5^wxl@Gvb){12bt3xyJL}M~R>6+G`b59uXyR5$T zdSHv%hM+XU2H5BU=jU>{Jzf%{s2pzQ$WTlhHoFtIIxJ$Q;WZ(tMrU>Cbo7^4>Q~h8 z{&D`EK@NX#=5cX6qmruyzmZ3?QRJ5l@V_U9{l{Fu@So&z_hKq<{*uAK*M0@ZVk+n!F{T@)#TD0Er&<8os1@xE)wGA z{Z#{&dZ$O~nfg)Xaoc%~7W#SRwp;GMe}hThNoQoKMrRTYG?G$ z?`ZFS*^v=doUaXR3pJ7j@HMQy_{c?3=LEb&H;nbA3M z?xvqQ_Nw)=`jV%y5Wrrz`hH!Z3(ra1la^28qHEDKJPN0gL6n>?J)4KYgou{<4Zxyy z*w3sUS6W`q8~eu*7{Ti}!1mv3jKcE>DYn!BzU6hg)ARKH6awx<2kXWZ1@{C`9j4rw zR)8Gt9G%KJJ=Dh2_5ytjKPf-wTI|X5OxTB_65CM~;8LwQ0DT4gd(LdX`R%qtbKR6w>TSAS)q=2lzV_epV8ylTQ&dxxxQcXu6d&xdC(#-6MA(vx}Ts;OV?BTDT zA>w7axHA@&w9cANJE*e3Q(F}A-^fYz)srv>AAVaS-Bru(mQrxq80!nHIuJQ&@G<59 z$X>bdOFJf@$L?`2$=$NIhC<6LulJ}~bE`t1W3XE=nKGH*S#;f1upCrNSv|10@7qTm z(>Y3d%D{6MAg!kz?s8O&kS;`n__WH*ADt2~N0{335gJ~(E6ffBU0%ermStGS;bU33 zT3T;&-!nuMO6=CSf-M*8D-t{-z&H3UROu4XL|0B_M3p=dU0F4$ZHNDS`Jx93juH0x z0I2rN{dtY0+3+XeASa|z(K~vMDMjB;L1t%t@l9;N-xCY2n;%Xv2Zb4i|C!GBu(x-8 zgw%Gux+iF$AOv|Idkd34GeKisWrrp6Y-pHci%J(k*)VKd#+0TA%V&XrTuqR@o@P8_%9obHTtRIM=^F zxs;Y47o3wSP?VC%HQ!<0`5#Ny+Z;{ZS~Hq|ehx}dv2RR^v8PDsP7vry#?b9ayMheD3DC!BNu9w@G67QYb_0V3dOZkdd?QrKdFg8kjd7qKmUmZBYVD@4JP!! zGVl!pYM*;Z<)?gs@N688N=^C#R^0W@t&wo*qxLTM`o@qLLtfz1G}R1G)>u^MU&wr+ zbPkBfJ<6IkSZVI%>Va2GC>AKqOdz589M*zmD)0)sEZ8hRd&`>F~S|WtnYj(%S4#^9^^Ma~|?au^bo* z*7u8*5d(I;*-mRh!B z-Eg)mwXTZn-I)-gHx(QsAtl~qF1YThF)i7B066iA4Ybhs6g~hm{}C0Ul$BO`BIbs| zjb$vkC^GOPO@9Dl!e6vyUiBuUj`wt4$q7~uX9OC#ioUM@N>X+w&`yr}RO=g>@N#Ec zl~n#Z7*d}=o(-E!UCZPr0#kC&6W4-nJz;dE+)-I16uUv5b1upxZpr~{Yw!JTv6_&n zTuSexIy}or+x185ChI!y;*c^vge8Td3V|lWGr}aN$TF~BGY$@gOAk~Fdef#9S!AJy zP8Co}WcTUli47ZpZ7N33-m^B!M9dw!f?{bJm`G&!|f zvo$01(Ozktk$oCZ2*^7VZMNU2^#N(Ogdw#y`$0qi_^v$+h6(nEc8{7klC;^TD0YJs zZPQI)Pk`%~H$Sc&G_-O{j2k1*MnMGE$=czNUBnK~!Rb?83bx!?Te1S@|`o;{K&cC6Cb0LG!*y{eK8(^H)}{ADB4 zh0+<(QR?R!=9IYJNG2AEp_a^txG(aG7?^}BRI4?YFnW`yhSgaCJMI{mA&VBR^UY4x zux@@LQf)dL=fAGb9IJ4{r{+;U5YWdJojTu^+`j5OdO>=aW1bJvk!~Xh4c~tg;?Bb! z;sxo2-RHuuE?ZXF{^p7njD)$-%}e-Wo}#itH4-@4cy|JWmwjBR!4#X#3B?zTZ*Y?nQw5UGDm$mA`2D7;HY8G)p=zuPRvBYX8Bw_X-{}-C*hqK-RW~)JKMJiDyLIkL<53ZV>4!k ztGIo+)@56IP|}t&qnv|;*#oabjnEAu^qAK2z}K5;j}L%?>O!jpPqd!;zL`UVvj!!8 zgP($C{=G+h(rJ@eTtizHwWC0>acn_qrXUI`U7>O+cq_G%Ks)r9c#CHKNZA)Y?ZwxO zsH)kRw?M=#S!5|9vg*LwDRk*lU6rS5f_fC^LPESa-puOv4?uLNduRJ@k49nR>2utH ziO?E^?v)n~VS~U388ozdSzJQr`Z1l-sw2E;hr{E;PX>Obgpf6>D@6=96F4e(>mtz97Q7C*u+V!^rEQD7Wby4(RZN6d}1nCFVH=39p!4D;PKmOTo^%3A|7ZV)e+hJZ~4B^>jb zX4-kW^VABK>~3z@$$>s+OtV~Iy7ohvEd{l>RrL|(q+ZF5vo(b+SO=~k3wx<914I6y zgGuS?(i$jRT&_GvxsGcwByF@UfZvYc2)%g*^z_U)X$J)z1ge#|#2;fymt_lA_hV1t zQ%vmC7jp1j63^l7&@s-}$mAo+!nIkM*QBy#!irQiBDWOh;^_oNFPrC4!tVfyAb9v> zt*fOrv|GC8x{BZqCv75i5vpcCmtd?2cJ^8nE{P^pmiACZxr))V@7mmj+PRdgs2isE zEgaY#LE2|}$Jqt)xu*p+S&s<@_55BQPX$z{!M&^~f@LawyE0?dJK!08N^_6PC;VxK zWT&&cAAmkr!e($W0SwhzY*LqzNGOibr@@Coe5F+Uz2Y#2@+_d-T+DJkeZ!<4wk!*< zs&?^PshNcYF$ADoNlKrHqlil-CAFcgwBoKd9+*n==OIO`RiMw~C9_b~Nj;QXhamW{ zc_FsZ~Pd{q~F@6p_z*LckG?TPk?R}S^opU*!Kqq#z1sl#nT>x) z?GE$TM7qX^+J4FhKgMC- zgvN*wA+zRK^;#7Tkfo0Jh0&t#F`EyukJ>sQEHb?91e7wW2_73eOCwZH8Dqg7XM%^Q zR7(x7N17##;u8EV{g6)h6v3lb(Z;Gt^=p>o*+aSg+NTurN-bDrUnLx&XCrD_3xP z1rYij%{qIPanywalA_Q)nb+-H^%86el(+2rv{G3P&N-7v1&p*gQ~yo#C_9RDhw|_-#`+f-rvtKe*Mf$rF+jC%-1VDF0`@rT%`9QB z_o#>jMUShB35?T5F&%0mf1;&U?}L*#eAdUr!rP=%bi1GH6KZNT{N_F3on6h$_bU{; zW>$0QtsXLf1H1IUXy8R`ofNdDQ+1o4`AJCMs^5&CG!}!5oMoKs)Z6wJa=x5!5ktPs~OP@m_ zZILHHPgYjTX99v#=&37bGp~jaM6}4uTNqtjL{ciXvY@2XrQNQj50C2;Q?1RxTA!aT znZjkybG}z8!fFZfYhHpnCmB0-b){YuR(6p*vnmI!^rRNiGGHD5tc*{F5yLd zumShO>xbr@$i%FrEVd}!yv>hIJAEc;U?@5HUgD~v)O=GwV{n2i9=~JObY}7E0!`MM zGqIKx`)a$moLV}!Dty2_>x^<)>A-QuPKJSp+sWyjRCpB9**So)YOh7^jaYD)w$@zg z(zv8Ciy(E^eX4%486J&^m2j2Lq^=IwG*II%3eB$I0kf^P>`+ji4uj=XYpR9vR&X;QnsygA<)mtpikK#aWrri9^`zc(S7j#Z}%jXWq>Ug}P$KVFqjNmYR zJV5TPE9lR0P2G$mTB3Hq`~Zw%+IpjV{@E~fYYXZM>25UQt!!|dwnP{U^i2RstIW)E zt#Ra1u1h1uJDGBETg2z9^s8dv{bYsujk~$iY)w{a@Cxdg(p{T%9f}W~zO2sQ-b$FM zu^ydKT+*&+3k7vz=27G(>GhpIpQCWmn(HK-5zoE^1bY~`ilJq2x0;lfJ!R!C>%9V7np+(Tw_&RYa~hzSpva?T0r>FpEpYVZZS8EKy$(`9si^N zi9+MymZLW%<5Sw>pVMJoNy(Z^d@Mfz#yK7j$;iu^_Ftb-3m(%>JDTWkNRcI=g-=0n z;w0ib2zKc*vsna-;>|J56;~%RZ6V78f2`|5vjvV>o^nbc+j0gqRwuR!==Ti;eln82 z%pglSchb5(dy?;ER}kGVg=*_nBEDAfQO%MrIAPKsCvRH?__f6!x>P7*yhwB*_$yDR zdS+wtFUYPo@wq(gv|-29X9eQ?D0O+4^~Uj!tnach3c5Dgx=8i-Cm*c%M}{$iAM)xF zsOxU-o^AOvDcb8zLQO3T)qtaW|)Rt59#cf$gIc7B}SWjL*>1^Aqp-db(xo{{q@&-L( zpD)ux`>lfnLTwsOga?+798%ONE*krsbKu%%8N*-W>|O3PNK2axE`BvKi*m6!GOQ6k z+={c*mx${(nm$_d`FfIL@~;rWEWK7~f?^l!UkD|DaXNhIfR?BY+thPFLG;ZES+_u1 zf8Wrs%uacN!3SXQ-~+%5gvK_Ba1e_$)m#Wh2T;_`x##Q~zGC*%u#~rK+r0_o>zHhI zUY!nJU$k+pL|(xSh)Qgi?Hu|saXa>UA+X|~iB(N7yHzMRc?WU3VbW}W@)>jYJmEz7 ztHkqW6cH~%MAa+#Wdl-?0$JPFneQ6jY7SNCscRk7kby zB|OTryozbBuEaNv>S9d^NbNt>is{6~ncAV*nJK{0YznZYN2n^jEU$@@# zUPQ(0;YN0e(Qj16i5NfHoE@kY+C?D%dbLRmM#xN&Xy@paRynuC=s%eXsucFF$TF4S z#B~6&xPNqQJ}`l@d_%&ygjuuZ72vnT85KKQv@^6$LuMh9IemL}GSRl#hES}P3_PAkF#eM>aRm}UsopQTC6%r9pKX{;aA!U`T&BA>n<2oS zy4E!Bvtez%(|zKM5#4ga&b1esTaBYFP@!p(nY)fg+7Tm`p8 zTWSl~6w7GMbB@H9M{xkqk}>R81z~9=_Zm3kigUA{n>(79h2r7fnml}NsF;bs%*1s) zp8-6Ir14ugwe@I+tQ(35ouybb97aG9DRAPrYbW3ebM3;Www;XrtXbfL#j4`?f7-bY zr>4>`8ah%0mQa*xp@_7I2n(S~4B7>BR0~DA zpwgQV>hA&WMxy@yf_<6f&6~_U^WOb#zT7i&&lz_3XwoalHhaf%jDZH|vp%hRP35s; z$&k-)?hO1nyA^MgxhdCZWl7Y;7vZ}avV7%M_Ig{pl!zI_yw+my5H-5bDSKjQ44FCZ zFg2Y3`8)CAw@dqD8BgQx_a;SItxECUd+r-F3+wz`5VX;Ho_XmO{U^oSi@mU#S&N?j z`?TtPnA<)ppia~UeYMua>v_f*6%Xoy5o;H+?u~%>uV4LWCGL*Co}&gQc5(2*R?Y}c zTJ-iAgKD~Ki=7iz>cq>;bF&0V{Va37AQ2UmpDncC@>4q0X|LRoVF*pu@aVIO46lsb zEL^ydbw^#qJx-L4HLD2AAAb7PNiOHVOu!?+8jYbxEP~iTl>BFOvUsYBdYID5cn{`% z{`=rO9DF)V1kqmqQOB7WyTIKoV9;*~7~nkvoWGlCe3xY(>i%)!>;u(*eQ2i|5rnpJ z!E1qb%BJwTqC(7E8hMqK`y+JwJgFE`Sk`>BmTlEdSsQF}u} zlI$`mGbw#83O!SLG`s`pWenIZqVi}SBO?+Q>ldEZ_x-r{=GeA|PZ%q%OhNkS z&(y!Wl7kufVn3O^@<@+hy3S**)pOWEf^z(#&X$b&vKY6EW_mb06P=i^-8d*^JV5o9w0OS%%b89xpuxX`P2a4Z~^0#+=sdQ zcM|_FbGUi@gYoBs^r3>={0SlZii15`E8FZrZ>ku%s5`g4;kuiw_qcn>Rly5Kb z6~PoQs_{}x8TPUle2$i4I6?p9ojSxVd#;_4{tL_XM(g);!h5$~Z#05=QaBUUX~wrz zI<{pbHN$!z25t@_GLgJp*eM&?*2o;fbKOPutF3M>8utjA2u2cE5`UQj-muGdwZP-^ zGk`$*Ndn5p4$xLOS~%!fI659^67a4ef;TN~X2WZ&2DQMAGDga7fyqX z4YhS+5uH-y>rWi{Q4a!$A=<5y<`0OUc=+;`iYV#TzeWs+QqUBq3E90X$sozZx{C@1 zWxf~YWy~T|4C;LGt-mFHaF5Rby`WvLa&?oeDd1_wo?+ML%6UEer%eO;7V90e!}QR0 zvAoHW-H)niJ$nkHQx%4j?bGVZ{Cg3t0uxjseFa4ENL4m!>Gi9R{chj6pr6A0VG0Au zXnCcYqiK>cl4R3+_j!cd!u4)ZsgyxQA&M|gy-1NbVm~H!vd=Nqw&nR@!oG#3jE!`x zgKHmmp3;b^Oe|dt?k?tOwMt?Oo~ZX%t}R*HntRD7Ya)h;tw0*b>xxA?$aV8xBdJJ5 zN)}5S3)T#6^hxZr8#q_{^jHf-n zPr7>1h3&N$moW;U2X6(PQU6c|o!45>iRU*{_o1aFl$RwZCpg7PjC%CL1>>njKuiAf z55ats2onob0T$RqXb-Ev%+}f(@TNl?D9th2wf=*^*#EvihO_dUOR*mszlCZP<#$~+ zE#*l1Fkyh*9ZJXkFae~mLyk~8S{3Neh-#4;R zV&|}@)KZg@;JK0uNzY!$F6V7;oO)4NGD>)i+fKLn5=vUfKbsMg?!8dEl-QW^*3$^e z_m$33LqIk^zT9u3q;Npo#iIG%n$TJOiMB3f%<#;O`QC~q71If8V154%Nk~U52kI)w8?o8no5yn?D8`qQ_qLWqdX0lQbR^78jaxupNxea z{67aIFTNv3r9T^FIhVK5RQ@dP&yw;dS?mxNs$e8{>5O~%cCp_(`|=?DFVaDwTXWoo zxiSlvDmFY4F}BEp63`h+PNWG|Jo7>Z$9QUme0peXt24W|y=dU;;+AD-J=sDX!z#we zc8#Pz!B!+lNZ2Wv3ORDAjp2ND(PZjo4MQg^)yMMyN5$2p)nLq$uxyloLW(l^C-tKv_WV{xCC~qRdY6-rW`D? zE-bUi1%=N7p&ShgV!P9ZVN2M)aU7F7qp^UO|b!0kYAWi zI74Z7mD93GLG-fG!g-$pdLykNYOl*=_STqjY3Lm3HRIV0Qakx(;`58f4xRQyfjJ4j zZ&c2?nwOMXP(CBPe2k*l8GW30ciA<+b;qTPk4aN+!;C>9e&VmfG;zPGtx^ABgZFDm z>@Okm9{wYdS30fVzUu1GC)A2mt3{E_2X)M^+AO^67AZY$Wux;jSyVD|2y20U2!B*5 zbE}n^Ew_Wap{C?bb)ok+&LY&=33L~FXwtmB5ydrGRWqZ03TCteCN~GS=Ig9!y0A$@ zrzratl^`19<}qQ%Sac%Qi58mNb>@k6r~CHk2T|cuj`cn$YFi;+HqN`3k$YPGzy8mS zOI1hI0~Hz#)GzIi6>4f{_g{emmHGWh)v2}VJ5*X_Ex>QX^pYbcvIo3eK?OZFZi>6QWFU4!kF-7B$TW-n{99eW!-5C z=^ZGCgd$QNYL#NarLSiB)=_6UP*3zVl}?U@3p1OSSVO+tgu6LDL||9e->%Pa)G$*& zz&MTNJ2B_LrRPt|FLpMv?Al(7ePNO(gB*x6f$sR}o3V;kx{si|<*9e!5c=bL<1KHx z!ZZyX&t}I(ftZ`h3zEdn$Qy}dP*uy=ak6W4U*}kbY6+L<3A9Uq+*g&Nb&$qFv&pir z;4?d?W25Ti*aeeK+)QpLo2F=%p6%5}b4`{?1$%xYzcPMhVxsEm=4tA6(M*Wzdd3C^ zu!cYH&AE_1_`aHT0C)r4);}Z5otnu0W9j1RCi-1r75O4k3*A{rPBgZdX zZna=X0gk$6{un(0>=HkCXyOOrI|<@K$pM|!cPjLcuONQ#QJXYeC&-Q!riI0iT-*A}W>Iay3>|a`l%zu4?o%<{1{H6^F#Reb^$*cf0M<< e@97=pZ|LTADHVqH} literal 0 HcmV?d00001 diff --git a/spec/integration/pr/test-pr-2563.spec.js b/spec/integration/pr/test-pr-2563.spec.js new file mode 100644 index 000000000..dfd152f31 --- /dev/null +++ b/spec/integration/pr/test-pr-2563.spec.js @@ -0,0 +1,19 @@ +const ExcelJS = verquire('exceljs'); + +const COMMENTS_AND_HTIMAGE_XLSX_FILE_NAME = './spec/out/comments-headerImage.test.xlsx'; +const COMMENTS_AND_HTIMAGE_AND_SHEETIMAGE_XLSX_FILE_NAME = + './spec/out/comments-headerImage-sheetimage.test.xlsx'; + +describe('pull request 2563', () => { + it('pull request 2563 - header and footer support image', async () => { + const wb = new ExcelJS.Workbook(); + await wb.xlsx.readFile('./spec/integration/data/comments-headerImage.xlsx'); + await wb.xlsx.writeFile(COMMENTS_AND_HTIMAGE_XLSX_FILE_NAME); + }); + + it('pull request 2563 - sheet image and hf image', async () => { + const wb = new ExcelJS.Workbook(); + await wb.xlsx.readFile('./spec/integration/data/comments-headerImage-sheetimage.xlsx'); + await wb.xlsx.writeFile(COMMENTS_AND_HTIMAGE_AND_SHEETIMAGE_XLSX_FILE_NAME); + }); +});