From 4f3b9e78331c4c126ac1006217040cae1e1d1a09 Mon Sep 17 00:00:00 2001 From: Michael Akopyants Date: Wed, 11 Jan 2023 16:31:54 +0100 Subject: [PATCH 1/4] xlsx stream add image --- lib/stream/xlsx/workbook-writer.js | 2 + lib/stream/xlsx/worksheet-writer.js | 47 +++++++++++++++++++++- lib/xlsx/rel-type.js | 1 + lib/xlsx/xform/core/content-types-xform.js | 3 +- lib/xlsx/xform/drawing/drawing-xform.js | 19 +++++++++ lib/xlsx/xform/sheet/image-xform.js | 33 +++++++++++++++ test/test-stream-addImage.js | 42 +++++++++++++++++++ 7 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 lib/xlsx/xform/sheet/image-xform.js create mode 100644 test/test-stream-addImage.js diff --git a/lib/stream/xlsx/workbook-writer.js b/lib/stream/xlsx/workbook-writer.js index 2d97cec88..7c43cdbbd 100644 --- a/lib/stream/xlsx/workbook-writer.js +++ b/lib/stream/xlsx/workbook-writer.js @@ -45,6 +45,7 @@ class WorkbookWriter { this.zipOptions = options.zip; this.media = []; + this.drawings = []; this.commentRefs = []; this.zip = Archiver('zip', this.zipOptions); @@ -219,6 +220,7 @@ class WorkbookWriter { sharedStrings: this.sharedStrings, commentRefs: this.commentRefs, media: this.media, + drawings: this.drawings, }; const xform = new ContentTypesXform(); const xml = xform.toXml(model); diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index 96705b7f0..fa0f74b8c 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -34,6 +34,10 @@ const PictureXform = require('../../xlsx/xform/sheet/picture-xform'); const ConditionalFormattingsXform = require('../../xlsx/xform/sheet/cf/conditional-formattings-xform'); const HeaderFooterXform = require('../../xlsx/xform/sheet/header-footer-xform'); const RowBreaksXform = require('../../xlsx/xform/sheet/row-breaks-xform'); +const ImageXForm = require('../../xlsx/xform/sheet/image-xform'); +const DrawingXform = require('../../xlsx/xform/drawing/drawing-xform'); +const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform'); +const Image = require('../../doc/image'); // since prepare and render are functional, we can use singletons const xform = { @@ -49,6 +53,7 @@ const xform = { pageSeteup: new PageSetupXform(), autoFilter: new AutoFilterXform(), picture: new PictureXform(), + drawing: new ImageXForm(), conditionalFormattings: new ConditionalFormattingsXform(), headerFooter: new HeaderFooterXform(), rowBreaks: new RowBreaksXform(), @@ -181,6 +186,7 @@ class WorksheetWriter { this.autoFilter = options.autoFilter || null; this._media = []; + this.anchors = []; // worksheet protection this.sheetProtection = null; @@ -249,7 +255,7 @@ class WorksheetWriter { // Legacy Data tag for comments this._writeLegacyData(); - + this._writeDrawings(); this._writeCloseWorksheet(); // signal end of stream to workbook this.stream.end(); @@ -469,6 +475,18 @@ class WorksheetWriter { } } + // ========================================================================= + addImage(imageId, range) { + const model = { + type: 'image', + imageId, + range, + }; + const im = new Image(this, model); + this._media.push(im); + this.anchors.push(im); + } + // ========================================================================= addBackgroundImage(imageId) { @@ -695,6 +713,33 @@ class WorksheetWriter { } } + _writeDrawings() { + if (this._media.length > 0 ) { + const {zip} = this.workbook; + + const drawingXform = new DrawingXform(); + const lastDrawingId = this.workbook.drawings.length + 1; + const relsXform = new RelationshipsXform(); + const xml = drawingXform.toXml(this); + zip.append(xml, {name: `xl/drawings/drawing${lastDrawingId}.xml`}); + const xmlRels = relsXform.toXml(this.anchors.map((a, index) => { + const image = this.workbook.getImage(a.imageId); + return { + Id: `rId${index + 1}`, + Type: RelType.Image, + Target: `../media/${image.name}`, + }; + })); + this.workbook.drawings.push({name: `drawing${lastDrawingId}`}); + zip.append(xmlRels, {name: `xl/drawings/_rels/drawing${lastDrawingId}.xml.rels`}); + const drawingId = this._sheetRelsWriter.addMedia({ + Target: `../drawings/drawing${lastDrawingId}.xml`, + Type: RelType.Drawing, + }); + this.stream.write(xform.drawing.toXml({rId: drawingId})); + } + } + _writeLegacyData() { if (this.hasComments) { xmlBuffer.reset(); diff --git a/lib/xlsx/rel-type.js b/lib/xlsx/rel-type.js index 7cd0a3d05..2493e39df 100644 --- a/lib/xlsx/rel-type.js +++ b/lib/xlsx/rel-type.js @@ -11,6 +11,7 @@ module.exports = { Theme: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme', Hyperlink: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', Image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', + Drawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing', CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', ExtenderProperties: diff --git a/lib/xlsx/xform/core/content-types-xform.js b/lib/xlsx/xform/core/content-types-xform.js index 2999c62aa..3aa199b7f 100644 --- a/lib/xlsx/xform/core/content-types-xform.js +++ b/lib/xlsx/xform/core/content-types-xform.js @@ -38,8 +38,9 @@ class ContentTypesXform extends BaseXform { PartName: name, ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml', }); + }); - + xmlStream.leafNode('Override', { PartName: '/xl/theme/theme1.xml', ContentType: 'application/vnd.openxmlformats-officedocument.theme+xml', diff --git a/lib/xlsx/xform/drawing/drawing-xform.js b/lib/xlsx/xform/drawing/drawing-xform.js index 6bc4c43d8..587436992 100644 --- a/lib/xlsx/xform/drawing/drawing-xform.js +++ b/lib/xlsx/xform/drawing/drawing-xform.js @@ -33,6 +33,25 @@ class DrawingXform extends BaseXform { return 'xdr:wsDr'; } + toXml(model) { + const xmlStream = new XmlStream(); + xmlStream.openXml(XmlStream.StdDocAttributes); + xmlStream.openNode(this.tag, DrawingXform.DRAWING_ATTRIBUTES); + + model.anchors.forEach((item, index) => { + item.picture = {rId: `rId${index + 1}`, index: index + 1}; + + + item.anchorType = getAnchorType(item); + const anchor = this.map[item.anchorType]; + + anchor.render(xmlStream, item); + }); + xmlStream.closeNode(); + + return xmlStream.xml; + } + render(xmlStream, model) { xmlStream.openXml(XmlStream.StdDocAttributes); xmlStream.openNode(this.tag, DrawingXform.DRAWING_ATTRIBUTES); diff --git a/lib/xlsx/xform/sheet/image-xform.js b/lib/xlsx/xform/sheet/image-xform.js new file mode 100644 index 000000000..62a7620ca --- /dev/null +++ b/lib/xlsx/xform/sheet/image-xform.js @@ -0,0 +1,33 @@ +const BaseXform = require('../base-xform'); + +class ImageXform extends BaseXform { + get tag() { + return 'drawing'; + } + + render(xmlStream, model) { + if (model) { + xmlStream.leafNode(this.tag, {'r:id': model.rId}); + } + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.model = { + rId: node.attributes['r:id'], + }; + return true; + default: + return false; + } + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = ImageXform; diff --git a/test/test-stream-addImage.js b/test/test-stream-addImage.js new file mode 100644 index 000000000..dc66ab06f --- /dev/null +++ b/test/test-stream-addImage.js @@ -0,0 +1,42 @@ +const path = require('path'); +const Excel = require('../lib/exceljs.nodejs'); +const HrStopwatch = require('./utils/hr-stopwatch'); + +const filename = process.argv[2]; + +const wb = new Excel.stream.xlsx.WorkbookWriter({filename}); +const ws1 = wb.addWorksheet('Foo'); +const imageId1 = wb.addImage({ + filename: path.join(__dirname, 'data/image2.png'), + extension: 'png', + }); +const imageId2 = wb.addImage({ + filename: path.join(__dirname, 'data/bubbles.jpg'), + extension: 'jpg', +}); + +ws1.addImage(imageId1, {tl: {col: 0.25, row: 0.7}, + ext: {width: 160, height: 60}}); + +ws1.addImage(imageId2, 'C1:F10'); + +const ws2 = wb.addWorksheet('Fooo2'); +ws2.addImage(imageId1, 'A1:B4'); +const imageId3 = wb.addImage({ + filename: path.join(__dirname, 'data/image2.png'), + extension: 'png', +}); +ws2.addImage(imageId3, 'B4:D10'); +const stopwatch = new HrStopwatch(); +stopwatch.start(); + +wb.commit() + .then(() => { + const micros = stopwatch.microseconds; + console.log('Done.'); + console.log('Time taken:', micros); + }) + .catch(error => { + console.log(error.message); + }); + From 2629b1b32e634edc17471d1f1350da2575b57baf Mon Sep 17 00:00:00 2001 From: Iakov Pustilnik Date: Thu, 12 Jan 2023 17:03:55 +0530 Subject: [PATCH 2/4] WorksheetWriter.addImage --- lib/stream/xlsx/workbook-writer.js | 21 +++++++ lib/stream/xlsx/worksheet-writer.js | 57 ++++++++++--------- lib/xlsx/xform/sheet/image-xform.js | 33 ----------- .../integration/worksheet-xlsx-writer.spec.js | 29 ++++++++++ 4 files changed, 81 insertions(+), 59 deletions(-) delete mode 100644 lib/xlsx/xform/sheet/image-xform.js diff --git a/lib/stream/xlsx/workbook-writer.js b/lib/stream/xlsx/workbook-writer.js index 7c43cdbbd..637ddf450 100644 --- a/lib/stream/xlsx/workbook-writer.js +++ b/lib/stream/xlsx/workbook-writer.js @@ -14,6 +14,7 @@ const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform'); const AppXform = require('../../xlsx/xform/core/app-xform'); const WorkbookXform = require('../../xlsx/xform/book/workbook-xform'); const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform'); +const DrawingXform = require('../../xlsx/xform/drawing/drawing-xform'); const WorksheetWriter = require('./worksheet-writer'); @@ -99,6 +100,7 @@ class WorkbookWriter { // commit all worksheets, then add suplimentary files await this.promise; await this.addMedia(); + await this.addDrawings(); await this._commitWorksheets(); await Promise.all([ this.addContentTypes(), @@ -251,6 +253,25 @@ class WorkbookWriter { ); } + addDrawings() { + return Promise.all( + this.drawings.map(drawing => { + const drawingXform = new DrawingXform(); + const relsXform = new RelationshipsXform(); + if (drawing) { + drawingXform.prepare(drawing, {}); + let xml = drawingXform.toXml(drawing); + xml = relsXform.toXml(drawing.rels); + return [ + this.zip.append(xml, {name: `xl/drawings/${drawing.name}.xml`}), + this.zip.append(xml, {name: `xl/drawings/_rels/${drawing.name}.xml.rels`}), + ]; + } + return null; + }).flat().filter(Boolean) + ); + } + addApp() { return new Promise(resolve => { const model = { diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index fa0f74b8c..2949c66a7 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -34,9 +34,7 @@ const PictureXform = require('../../xlsx/xform/sheet/picture-xform'); const ConditionalFormattingsXform = require('../../xlsx/xform/sheet/cf/conditional-formattings-xform'); const HeaderFooterXform = require('../../xlsx/xform/sheet/header-footer-xform'); const RowBreaksXform = require('../../xlsx/xform/sheet/row-breaks-xform'); -const ImageXForm = require('../../xlsx/xform/sheet/image-xform'); const DrawingXform = require('../../xlsx/xform/drawing/drawing-xform'); -const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform'); const Image = require('../../doc/image'); // since prepare and render are functional, we can use singletons @@ -53,7 +51,7 @@ const xform = { pageSeteup: new PageSetupXform(), autoFilter: new AutoFilterXform(), picture: new PictureXform(), - drawing: new ImageXForm(), + drawing: new DrawingXform(), conditionalFormattings: new ConditionalFormattingsXform(), headerFooter: new HeaderFooterXform(), rowBreaks: new RowBreaksXform(), @@ -250,12 +248,13 @@ class WorksheetWriter { this._writePageMargins(); this._writePageSetup(); this._writeBackground(); + this._writeDrawings(); this._writeHeaderFooter(); this._writeRowBreaks(); // Legacy Data tag for comments this._writeLegacyData(); - this._writeDrawings(); + this._writeCloseWorksheet(); // signal end of stream to workbook this.stream.end(); @@ -487,7 +486,9 @@ class WorksheetWriter { this.anchors.push(im); } - // ========================================================================= + getImages() { + return this._media.filter(m => m.type === 'image'); + } addBackgroundImage(imageId) { this._background = { @@ -714,29 +715,33 @@ class WorksheetWriter { } _writeDrawings() { - if (this._media.length > 0 ) { - const {zip} = this.workbook; - - const drawingXform = new DrawingXform(); - const lastDrawingId = this.workbook.drawings.length + 1; - const relsXform = new RelationshipsXform(); - const xml = drawingXform.toXml(this); - zip.append(xml, {name: `xl/drawings/drawing${lastDrawingId}.xml`}); - const xmlRels = relsXform.toXml(this.anchors.map((a, index) => { - const image = this.workbook.getImage(a.imageId); - return { - Id: `rId${index + 1}`, - Type: RelType.Image, - Target: `../media/${image.name}`, + if (this._media.length) { + this.getImages().forEach((image, i) => { + const lastDrawingId = this.workbook.drawings.length + 1; + const drawingId = this._sheetRelsWriter.addMedia({ + Target: `../drawings/drawing${lastDrawingId}.xml`, + Type: RelType.Drawing, + }); + const imageObj = this.workbook.getImage(image.imageId); + const drawing = { + rId: drawingId, + name: `drawing${lastDrawingId}`, + rels: [{ + Id: 'rId1', + Type: RelType.Image, + Target: `../media/${imageObj.name}`, + }], + anchors: [{ + picture: { + rId: image.imageId, + }, + range: image.range, + }], }; - })); - this.workbook.drawings.push({name: `drawing${lastDrawingId}`}); - zip.append(xmlRels, {name: `xl/drawings/_rels/drawing${lastDrawingId}.xml.rels`}); - const drawingId = this._sheetRelsWriter.addMedia({ - Target: `../drawings/drawing${lastDrawingId}.xml`, - Type: RelType.Drawing, + this.workbook.drawings.push(drawing); + xform.drawing.prepare(drawing, {}); + this.stream.write(xform.drawing.toXml(drawing)); }); - this.stream.write(xform.drawing.toXml({rId: drawingId})); } } diff --git a/lib/xlsx/xform/sheet/image-xform.js b/lib/xlsx/xform/sheet/image-xform.js deleted file mode 100644 index 62a7620ca..000000000 --- a/lib/xlsx/xform/sheet/image-xform.js +++ /dev/null @@ -1,33 +0,0 @@ -const BaseXform = require('../base-xform'); - -class ImageXform extends BaseXform { - get tag() { - return 'drawing'; - } - - render(xmlStream, model) { - if (model) { - xmlStream.leafNode(this.tag, {'r:id': model.rId}); - } - } - - parseOpen(node) { - switch (node.name) { - case this.tag: - this.model = { - rId: node.attributes['r:id'], - }; - return true; - default: - return false; - } - } - - parseText() {} - - parseClose() { - return false; - } -} - -module.exports = ImageXform; diff --git a/spec/integration/worksheet-xlsx-writer.spec.js b/spec/integration/worksheet-xlsx-writer.spec.js index 38d743389..7e18f119a 100644 --- a/spec/integration/worksheet-xlsx-writer.spec.js +++ b/spec/integration/worksheet-xlsx-writer.spec.js @@ -1,3 +1,4 @@ +const path = require('path'); const testutils = require('../utils/index'); const ExcelJS = verquire('exceljs'); @@ -535,4 +536,32 @@ describe('WorksheetWriter', () => { expect(ws.rowBreaks.length).to.equal(2); }); }); + + describe('Images/Drawings', () => { + it.skip('add images', async () => { + // const wb = new ExcelJS.stream.xlsx.WorkbookWriter(); + const filename = path.join(__dirname, 'test.xlsx'); + const wb = new ExcelJS.stream.xlsx.WorkbookWriter({filename, useStyles: true, useSharedStrings: true}); + const ws1 = wb.addWorksheet('foo'); + const ws2 = wb.addWorksheet('bar'); + + const imageId1 = wb.addImage({ + filename: path.join(__dirname, 'data/image.png'), + extension: 'png', + }); + const imageId2 = wb.addImage({ + filename: path.join(__dirname, 'data/bubbles.jpg'), + extension: 'jpg', + }); + ws1.addImage(imageId1, { + tl: {col: 0.25, row: 0.7}, + ext: {width: 160, height: 60}, + }); + ws2.addImage(imageId2, 'C1:F10'); + + ws1.commit(); + ws2.commit(); + await wb.commit(); + }); + }); }); From 5d2858e9cee07cb7e2bc6ce1203315626739e82b Mon Sep 17 00:00:00 2001 From: Iakov Pustilnik Date: Thu, 12 Jan 2023 17:18:09 +0530 Subject: [PATCH 3/4] revert lib/xlsx/xform/drawing/drawing-xform.js --- lib/xlsx/xform/drawing/drawing-xform.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/xlsx/xform/drawing/drawing-xform.js b/lib/xlsx/xform/drawing/drawing-xform.js index 587436992..6bc4c43d8 100644 --- a/lib/xlsx/xform/drawing/drawing-xform.js +++ b/lib/xlsx/xform/drawing/drawing-xform.js @@ -33,25 +33,6 @@ class DrawingXform extends BaseXform { return 'xdr:wsDr'; } - toXml(model) { - const xmlStream = new XmlStream(); - xmlStream.openXml(XmlStream.StdDocAttributes); - xmlStream.openNode(this.tag, DrawingXform.DRAWING_ATTRIBUTES); - - model.anchors.forEach((item, index) => { - item.picture = {rId: `rId${index + 1}`, index: index + 1}; - - - item.anchorType = getAnchorType(item); - const anchor = this.map[item.anchorType]; - - anchor.render(xmlStream, item); - }); - xmlStream.closeNode(); - - return xmlStream.xml; - } - render(xmlStream, model) { xmlStream.openXml(XmlStream.StdDocAttributes); xmlStream.openNode(this.tag, DrawingXform.DRAWING_ATTRIBUTES); From ba2c980e1718a75854f4caf20de6ce948c9be723 Mon Sep 17 00:00:00 2001 From: Iakov Pustilnik Date: Thu, 12 Jan 2023 18:59:02 +0530 Subject: [PATCH 4/4] WorksheetWriter.addImage --- lib/stream/xlsx/workbook-writer.js | 8 +++--- lib/stream/xlsx/worksheet-writer.js | 38 +++++++++++++++-------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/stream/xlsx/workbook-writer.js b/lib/stream/xlsx/workbook-writer.js index 637ddf450..37bbd7f0e 100644 --- a/lib/stream/xlsx/workbook-writer.js +++ b/lib/stream/xlsx/workbook-writer.js @@ -100,8 +100,8 @@ class WorkbookWriter { // commit all worksheets, then add suplimentary files await this.promise; await this.addMedia(); - await this.addDrawings(); await this._commitWorksheets(); + await this.addDrawings(); await Promise.all([ this.addContentTypes(), this.addApp(), @@ -260,11 +260,11 @@ class WorkbookWriter { const relsXform = new RelationshipsXform(); if (drawing) { drawingXform.prepare(drawing, {}); - let xml = drawingXform.toXml(drawing); - xml = relsXform.toXml(drawing.rels); + const xml = drawingXform.toXml(drawing); + const xmlRels = relsXform.toXml(drawing.rels); return [ this.zip.append(xml, {name: `xl/drawings/${drawing.name}.xml`}), - this.zip.append(xml, {name: `xl/drawings/_rels/${drawing.name}.xml.rels`}), + this.zip.append(xmlRels, {name: `xl/drawings/_rels/${drawing.name}.xml.rels`}), ]; } return null; diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index 2949c66a7..683cd941c 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -184,7 +184,6 @@ class WorksheetWriter { this.autoFilter = options.autoFilter || null; this._media = []; - this.anchors = []; // worksheet protection this.sheetProtection = null; @@ -483,7 +482,6 @@ class WorksheetWriter { }; const im = new Image(this, model); this._media.push(im); - this.anchors.push(im); } getImages() { @@ -716,32 +714,36 @@ class WorksheetWriter { _writeDrawings() { if (this._media.length) { - this.getImages().forEach((image, i) => { + const images = this.getImages(); const lastDrawingId = this.workbook.drawings.length + 1; const drawingId = this._sheetRelsWriter.addMedia({ Target: `../drawings/drawing${lastDrawingId}.xml`, Type: RelType.Drawing, }); - const imageObj = this.workbook.getImage(image.imageId); const drawing = { rId: drawingId, name: `drawing${lastDrawingId}`, - rels: [{ - Id: 'rId1', - Type: RelType.Image, - Target: `../media/${imageObj.name}`, - }], - anchors: [{ - picture: { - rId: image.imageId, - }, - range: image.range, - }], + rels: images.map((img, index) => { + const image = this.workbook.getImage(img.imageId); + return { + Id: `rId${index + 1}`, + Type: RelType.Image, + Target: `../media/${image.name}`, + }; + }), + anchors: images.map((img, index) => { + return { + picture: { + rId: `rId${index+1}`, + }, + range: img.range, + }; + }), }; this.workbook.drawings.push(drawing); - xform.drawing.prepare(drawing, {}); - this.stream.write(xform.drawing.toXml(drawing)); - }); + xmlBuffer.reset(); + xmlBuffer.addText(``); + this.stream.write(xmlBuffer); } }