diff --git a/lib/stream/xlsx/workbook-writer.js b/lib/stream/xlsx/workbook-writer.js index 2d97cec88..37bbd7f0e 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'); @@ -45,6 +46,7 @@ class WorkbookWriter { this.zipOptions = options.zip; this.media = []; + this.drawings = []; this.commentRefs = []; this.zip = Archiver('zip', this.zipOptions); @@ -99,6 +101,7 @@ class WorkbookWriter { await this.promise; await this.addMedia(); await this._commitWorksheets(); + await this.addDrawings(); await Promise.all([ this.addContentTypes(), this.addApp(), @@ -219,6 +222,7 @@ class WorkbookWriter { sharedStrings: this.sharedStrings, commentRefs: this.commentRefs, media: this.media, + drawings: this.drawings, }; const xform = new ContentTypesXform(); const xml = xform.toXml(model); @@ -249,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, {}); + 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(xmlRels, {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 96705b7f0..683cd941c 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -34,6 +34,8 @@ 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 DrawingXform = require('../../xlsx/xform/drawing/drawing-xform'); +const Image = require('../../doc/image'); // since prepare and render are functional, we can use singletons const xform = { @@ -49,6 +51,7 @@ const xform = { pageSeteup: new PageSetupXform(), autoFilter: new AutoFilterXform(), picture: new PictureXform(), + drawing: new DrawingXform(), conditionalFormattings: new ConditionalFormattingsXform(), headerFooter: new HeaderFooterXform(), rowBreaks: new RowBreaksXform(), @@ -244,6 +247,7 @@ class WorksheetWriter { this._writePageMargins(); this._writePageSetup(); this._writeBackground(); + this._writeDrawings(); this._writeHeaderFooter(); this._writeRowBreaks(); @@ -470,6 +474,19 @@ class WorksheetWriter { } // ========================================================================= + addImage(imageId, range) { + const model = { + type: 'image', + imageId, + range, + }; + const im = new Image(this, model); + this._media.push(im); + } + + getImages() { + return this._media.filter(m => m.type === 'image'); + } addBackgroundImage(imageId) { this._background = { @@ -695,6 +712,41 @@ class WorksheetWriter { } } + _writeDrawings() { + if (this._media.length) { + 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 drawing = { + rId: drawingId, + name: `drawing${lastDrawingId}`, + 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); + xmlBuffer.reset(); + xmlBuffer.addText(``); + this.stream.write(xmlBuffer); + } + } + _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/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(); + }); + }); }); 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); + }); +