From ba3540f57ec3a6c0315885b61bb657f4c8196d2e Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 12:01:49 +0100 Subject: [PATCH 01/10] adds "template" property to chart --- lib/chart.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/chart.js b/lib/chart.js index 5abadbd0..4152c602 100644 --- a/lib/chart.js +++ b/lib/chart.js @@ -27,6 +27,12 @@ class Chart extends ElementProperties { this.content = content; super.setPropertyContent(this.content['p:graphicFrame'][0]['p:xfrm'][0]); } + + template(templateParams) { + this.templateParams = templateParams; + + return this; + } } module.exports.Chart = Chart; From c42ccb39798ee5d4d6b487122e79caa107d61f0b Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 12:02:25 +0100 Subject: [PATCH 02/10] adds template-helper class --- lib/helpers/template-helper.js | 96 ++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 lib/helpers/template-helper.js diff --git a/lib/helpers/template-helper.js b/lib/helpers/template-helper.js new file mode 100644 index 00000000..c066ad8d --- /dev/null +++ b/lib/helpers/template-helper.js @@ -0,0 +1,96 @@ +/* eslint-disable no-prototype-builtins */ +const xml2js = require('xml2js'); +const JSZip = require('jszip'); +const fs = require('fs'); + +class TemplateHelper { + + static async applyTemplateXml(params, target) { + const parser = new xml2js.Parser({}); + const zip = new JSZip(); + + const content = fs.readFileSync(params.pptxFile) + const unzipped = await zip.loadAsync(content) + const xmlBuffer = await unzipped.files[params.xmlFile].async('nodebuffer') + const callback = TemplateHelper.getTemplateCallback(params, target) + + parser.parseString(xmlBuffer, function (err, templateXml) { + callback(templateXml, target) + }) + } + + static getTemplateCallback(params, target) { + if(params.callback === undefined) { + if(target.chart) { + return TemplateHelper.applyChartTemplate + } + } else { + target.TemplateHelper = TemplateHelper + return params.callback + } + } + + static applyChartTemplate(template, {newChartSpaceBlock, chart, chartType}) { + chartType = (chartType === undefined) + ? TemplateHelper.detectChartType(template) + : chartType + + TemplateHelper.applyChartSpace(template, newChartSpaceBlock) + + let seriesTemplate = TemplateHelper.getSeriesTemplate(template, chartType) + let seriesData = TemplateHelper.createSeriesFromTemplate(chart.chartData, seriesTemplate) + + TemplateHelper.applySeriesToChart(seriesData, newChartSpaceBlock, chartType) + } + + static createSeriesFromTemplate(chartData, seriesTemplate) { + const callback = TemplateHelper.getPptFactoryHelper().createSingleSeriesDataNode + return chartData.map(callback, { template: seriesTemplate }) + } + + static detectChartType(template) { + const plotArea = template['c:chartSpace']['c:chart'][0]['c:plotArea'][0] + for(let tag in plotArea) { + if(plotArea[tag][0]['c:ser']) { + return tag.replace('c:', '') + } + } + return 'barChart' + } + + static applyChartSpace(template, newChartSpaceBlock) { + newChartSpaceBlock['c:chartSpace'] = template['c:chartSpace'] + } + + static getSeriesTemplate(template, chartType) { + return template['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] + } + + static applySeriesToChart(series, newChartSpaceBlock, chartType) { + newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] = series + } + + static setTemplateSeriesDefault(templateSeries) { + let series = JSON.parse(JSON.stringify(templateSeries)) + if(typeof series['c:cat'] === undefined) { + series['c:cat'] = [ '' ] + } + return series + } + + static setTemplateSeriesData(series, {i, tx, cat, val}) { + series['c:idx'][0]['$']['val'] = i + series['c:order'][0]['$']['val'] = i + series['c:tx'][0] = tx + series['c:cat'][0] = cat + series['c:val'][0] = val + } + + static getPptFactoryHelper() { + const { PptFactoryHelper } = require('./ppt-factory-helper'); + return PptFactoryHelper + } + +} + +module.exports.TemplateHelper = TemplateHelper; \ No newline at end of file From fc0522f63bbd5e5920ad6ef230dbb9d1f5a12589 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 12:03:09 +0100 Subject: [PATCH 03/10] introduces TemplateHelper to addChart --- lib/factories/ppt/slides.js | 13 +++++++++---- lib/helpers/ppt-factory-helper.js | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/factories/ppt/slides.js b/lib/factories/ppt/slides.js index fb517a42..066a4f10 100644 --- a/lib/factories/ppt/slides.js +++ b/lib/factories/ppt/slides.js @@ -2,6 +2,7 @@ let { PptFactoryHelper } = require('../../helpers/ppt-factory-helper'); let { PptxContentHelper } = require('../../helpers/pptx-content-helper'); +let { TemplateHelper } = require('../../helpers/template-helper'); class SlideFactory { constructor(parentFactory, args) { @@ -306,15 +307,19 @@ class SlideFactory { return newShapeBlock; } - addChart(slide, chart) { + async addChart(slide, chart) { let slideKey = `ppt/slides/${slide.name}.xml`; let chartKey = `ppt/charts/${chart.name}.xml`; let newGraphicFrameBlock = PptFactoryHelper.createBaseChartFrameBlock(chart.x(), chart.y(), chart.cx(), chart.cy()); // goes onto the slide let newChartSpaceBlock = PptFactoryHelper.createBaseChartSpaceBlock(); // goes into the chart XML - let seriesDataBlock = PptFactoryHelper.createSeriesDataBlock(chart.chartData); - - newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:barChart'][0]['c:ser'] = seriesDataBlock['c:ser']; + + if(chart.templateParams !== undefined) { + await TemplateHelper.applyTemplateXml(chart.templateParams, { newChartSpaceBlock, chart }) + } else { + let seriesDataBlock = PptFactoryHelper.createSeriesDataBlock(chart.chartData); + newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:barChart'][0]['c:ser'] = seriesDataBlock['c:ser']; + } this.content[chartKey] = newChartSpaceBlock; this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0]['p:graphicFrame'] = newGraphicFrameBlock['p:graphicFrame']; diff --git a/lib/helpers/ppt-factory-helper.js b/lib/helpers/ppt-factory-helper.js index e97bd235..1c37a5ab 100644 --- a/lib/helpers/ppt-factory-helper.js +++ b/lib/helpers/ppt-factory-helper.js @@ -5,6 +5,7 @@ const XmlNode = require('../xmlnode'); let { ExcelHelper } = require('./excel-helper'); let { PptxUnitHelper } = require('./unit-helper'); +let { TemplateHelper } = require('./template-helper'); const HyperlinkType = { TEXT: 'text', @@ -634,13 +635,23 @@ class PptFactoryHelper { let sheetCellRangeForCategories = `Sheet1!${rc2a(2, 1, true, true)}:${rc2a(2 + series.labels.length - 1, 1, true, true)}`; let sheetCellAddressForSeriesName = `Sheet1!${rc2a(1, 2 + i, true, true)}`; + let tx = strRef(sheetCellAddressForSeriesName, [series.name]) + let cat = strRef(sheetCellRangeForCategories, series.labels) + let val = numRef(sheetCellRangeForValues, series.values, 'General') + + if(typeof this.template[i] !== undefined) { + let series = TemplateHelper.setTemplateSeriesDefault(this.template[i]) + TemplateHelper.setTemplateSeriesData(series, {i, tx, cat, val}) + return series + } + let serChildNodes = XmlNode() .addChild('c:idx', XmlNode().attr('val', i)) .addChild('c:order', XmlNode().attr('val', i)) - .addChild('c:tx', strRef(sheetCellAddressForSeriesName, [series.name])) .addChild('c:invertIfNegative', XmlNode().attr('val', 0)) - .addChild('c:cat', strRef(sheetCellRangeForCategories, series.labels)) - .addChild('c:val', numRef(sheetCellRangeForValues, series.values, 'General')); + .addChild('c:tx', tx) + .addChild('c:cat', cat) + .addChild('c:val', val); if (series.color) { let colorBlock = PptFactoryHelper.createColorBlock(series.color); From bfb8bbceda8acb55ea209aad3f01c57877db6fb2 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 16:19:29 +0100 Subject: [PATCH 04/10] adds comments to template related functions --- lib/factories/ppt/slides.js | 1 + lib/helpers/template-helper.js | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/factories/ppt/slides.js b/lib/factories/ppt/slides.js index 066a4f10..cc410918 100644 --- a/lib/factories/ppt/slides.js +++ b/lib/factories/ppt/slides.js @@ -315,6 +315,7 @@ class SlideFactory { let newChartSpaceBlock = PptFactoryHelper.createBaseChartSpaceBlock(); // goes into the chart XML if(chart.templateParams !== undefined) { + // If templateParams are set, a chart template will be loaded and applied to newChartSpaceBlock. await TemplateHelper.applyTemplateXml(chart.templateParams, { newChartSpaceBlock, chart }) } else { let seriesDataBlock = PptFactoryHelper.createSeriesDataBlock(chart.chartData); diff --git a/lib/helpers/template-helper.js b/lib/helpers/template-helper.js index c066ad8d..34a1fdbb 100644 --- a/lib/helpers/template-helper.js +++ b/lib/helpers/template-helper.js @@ -3,8 +3,12 @@ const xml2js = require('xml2js'); const JSZip = require('jszip'); const fs = require('fs'); +// This helper class bundles functions needed to read, transform and apply xml templates. class TemplateHelper { + // We need to load, extract and parse xml data from a given pptx file + // Currently, only charts can be edited this way. + // TODO: also non-zipped xml data should be processed. static async applyTemplateXml(params, target) { const parser = new xml2js.Parser({}); const zip = new JSZip(); @@ -13,12 +17,14 @@ class TemplateHelper { const unzipped = await zip.loadAsync(content) const xmlBuffer = await unzipped.files[params.xmlFile].async('nodebuffer') const callback = TemplateHelper.getTemplateCallback(params, target) - + parser.parseString(xmlBuffer, function (err, templateXml) { callback(templateXml, target) }) } + // When there is no callback defined, we will apply a default callback. + // The default callback will depend on the type of shape; atm only charts are supported. static getTemplateCallback(params, target) { if(params.callback === undefined) { if(target.chart) { @@ -30,6 +36,7 @@ class TemplateHelper { } } + // This is the default callback to inject template xml data into a newChartSpaceBlock static applyChartTemplate(template, {newChartSpaceBlock, chart, chartType}) { chartType = (chartType === undefined) ? TemplateHelper.detectChartType(template) @@ -43,11 +50,15 @@ class TemplateHelper { TemplateHelper.applySeriesToChart(seriesData, newChartSpaceBlock, chartType) } + // This will refer to getPptFactoryHelper.createSingleSeriesDataNode. + // A check for this.template will fork back here and call TemplateHelper.setTemplateSeriesData static createSeriesFromTemplate(chartData, seriesTemplate) { const callback = TemplateHelper.getPptFactoryHelper().createSingleSeriesDataNode return chartData.map(callback, { template: seriesTemplate }) } + // If we don't have to deal with combo charts, the chart type can be auto-detected. + // The first parent of a series will return the cart type. static detectChartType(template) { const plotArea = template['c:chartSpace']['c:chart'][0]['c:plotArea'][0] for(let tag in plotArea) { @@ -58,18 +69,22 @@ class TemplateHelper { return 'barChart' } + // This will replace the entire chartSpace by the given template xml. static applyChartSpace(template, newChartSpaceBlock) { newChartSpaceBlock['c:chartSpace'] = template['c:chartSpace'] } + // We want to pick the series by chart type. static getSeriesTemplate(template, chartType) { return template['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] } + // This is to replace the template series by a calculated one. static applySeriesToChart(series, newChartSpaceBlock, chartType) { newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] = series } + // We have to clone the template series get a fresh copy. static setTemplateSeriesDefault(templateSeries) { let series = JSON.parse(JSON.stringify(templateSeries)) if(typeof series['c:cat'] === undefined) { @@ -78,6 +93,7 @@ class TemplateHelper { return series } + // This will apply the data from PptFactoryHelper.createSingleSeriesDataNode static setTemplateSeriesData(series, {i, tx, cat, val}) { series['c:idx'][0]['$']['val'] = i series['c:order'][0]['$']['val'] = i @@ -86,6 +102,9 @@ class TemplateHelper { series['c:val'][0] = val } + // We need this, because + // const { PptFactoryHelper } = require('./ppt-factory-helper') + // on top of this file will return undefined and I have no clue why. static getPptFactoryHelper() { const { PptFactoryHelper } = require('./ppt-factory-helper'); return PptFactoryHelper From f038ceeca99f6f8ff5a252d1867281b89d096bcf Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 16:20:22 +0100 Subject: [PATCH 05/10] fixes calling conditions for template series --- lib/helpers/ppt-factory-helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/helpers/ppt-factory-helper.js b/lib/helpers/ppt-factory-helper.js index 1c37a5ab..f46a9aa8 100644 --- a/lib/helpers/ppt-factory-helper.js +++ b/lib/helpers/ppt-factory-helper.js @@ -626,6 +626,7 @@ class PptFactoryHelper { } // this will return all the child nodes that belong under a node (it will NOT contain the root) + // Passing an array in this.template will return series created by TemplateHelper. static createSingleSeriesDataNode(series, i) { let rc2a = ExcelHelper.rowColToSheetAddress; let strRef = PptFactoryHelper.createStrRefNode; @@ -639,7 +640,7 @@ class PptFactoryHelper { let cat = strRef(sheetCellRangeForCategories, series.labels) let val = numRef(sheetCellRangeForValues, series.values, 'General') - if(typeof this.template[i] !== undefined) { + if(typeof this.template !== 'undefined' && typeof this.template[i] !== 'undefined') { let series = TemplateHelper.setTemplateSeriesDefault(this.template[i]) TemplateHelper.setTemplateSeriesData(series, {i, tx, cat, val}) return series From 67b24c789d98238d48a2b5054c4deb416bb85c62 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 16:20:58 +0100 Subject: [PATCH 06/10] adds tests for template charts --- __tests__/fixtures/template-charts.pptx | Bin 0 -> 54513 bytes __tests__/template-chart.test.js | 170 ++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 __tests__/fixtures/template-charts.pptx create mode 100644 __tests__/template-chart.test.js diff --git a/__tests__/fixtures/template-charts.pptx b/__tests__/fixtures/template-charts.pptx new file mode 100644 index 0000000000000000000000000000000000000000..498b26878f7de9d2a97cae0e15efbfbc0838ed72 GIT binary patch literal 54513 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%0DH$BqEAkRMt#bb5bY|!41Xw! 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%0DXBC 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 w@H;UGq}BO@0{xj1{Aau3XS?@D>inJ`sK4*3{E< { + beforeAll(() => { + prepareTmpDir(tmpDir); + }); + + test('should be able to create a simple chart and apply a bar chart template', async () => { + try { + expect.assertions(1); + + let pptx = new PPTX.Composer(); + let promise = (await pptx.compose(async pres => { + await pres.layout('LAYOUT_4x3').addSlide(async slide => { + await slide.addChart(chart => { + chart + .template({ + pptxFile: pptxTemplateFile, + xmlFile: 'ppt/charts/chart1.xml' + }) + .data(barChartData1) + .x(100) + .y(100) + .cx(400) + .cy(300); + }); + }); + + })).save(`${tmpDir}/${tmpFile}`); + + await promise; + + expect(fs.existsSync(`${tmpDir}/${tmpFile}`)).toBe(true); + } catch (err) { + console.warn(err); + throw err; + } + }); + + test('should be able to create a chart and apply a pie chart template', async () => { + try { + expect.assertions(1); + + let pptx = new PPTX.Composer(); + let promise = (await pptx.compose(async pres => { + await pres.layout('LAYOUT_4x3').addSlide(async slide => { + await slide.addChart(chart => { + chart + .template({ + pptxFile: pptxTemplateFile, + xmlFile: 'ppt/charts/chart2.xml' + }) + .data(pieChartData) + .x(100) + .y(100) + .cx(400) + .cy(300); + }); + }); + + })).save(`${tmpDir}/${tmpFile}`); + + await promise; + + expect(fs.existsSync(`${tmpDir}/${tmpFile}`)).toBe(true); + } catch (err) { + console.warn(err); + throw err; + } + }); + + test('should be able to create a chart and apply a combo chart template', async () => { + try { + expect.assertions(1); + + let pptx = new PPTX.Composer(); + let promise = (await pptx.compose(async pres => { + await pres.layout('LAYOUT_4x3').addSlide(async slide => { + await slide.addChart(chart => { + chart + .template({ + pptxFile: pptxTemplateFile, + xmlFile: 'ppt/charts/chart3.xml', + callback: (template, { newChartSpaceBlock, TemplateHelper, chart }) => { + // First we replace chartSpace by our template + TemplateHelper.applyChartSpace(template, newChartSpaceBlock) + + // We need to extract the different series templates using their chart type names + let seriesTemplateBar = TemplateHelper.getSeriesTemplate(template, 'barChart') + let seriesTemplateLine = TemplateHelper.getSeriesTemplate(template, 'lineChart') + + // Combined, the templates will be passed to the creator + seriesTemplate = [ + seriesTemplateBar[0], seriesTemplateBar[1], seriesTemplateLine[0] + ] + + // The series have to be calculated based on the passed series templates + let series = TemplateHelper.createSeriesFromTemplate(chart.chartData, seriesTemplate) + + // We will override the template series with our created ones + TemplateHelper.applySeriesToChart([series[0], series[1]], newChartSpaceBlock, 'barChart') + TemplateHelper.applySeriesToChart([series[2]], newChartSpaceBlock, 'lineChart') + } + }) + .data(barChartData1) + .x(100) + .y(100) + .cx(400) + .cy(300) + }); + }); + + })).save(`${tmpDir}/${tmpFile}`); + + await promise; + + expect(fs.existsSync(`${tmpDir}/${tmpFile}`)).toBe(true); + } catch (err) { + console.warn(err); + throw err; + } + }); +}); + +function prepareTmpDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } else { + emptyDir(dir); + } +} + +function emptyDir(dir) { + if (fs.existsSync(`${__dirname}/tmp/${tmpFile}`)) { + fs.unlink(`${__dirname}/tmp/${tmpFile}`, err => { + if (err) throw err; + }); + } +} From d0172d18177d51cf629628dca30ebe33cb87b49c Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 16:48:02 +0100 Subject: [PATCH 07/10] some info about template-charts --- readme.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/readme.md b/readme.md index 0363cc7c..497747ad 100644 --- a/readme.md +++ b/readme.md @@ -355,6 +355,53 @@ await pptx .save('./chart.pptx'); ``` +Although, you can use custom chart templates. Those will be imported from any pptx file and applied using an optional callback function. So, you don't have to wait for further chart types to be implemented, but reuse any existing chart. All you need to know is the chart-id inside the pptx file. Please refer to template-chart.test.js for further examples and handling combo chart types. + +#### Bar Charts + +```javascript +const PPTX = require('nodejs-pptx'); +let pptx = new PPTX.Composer(); + +let barChartData1 = [ + { + name: 'Series 1', + labels: ['Category 1', 'Category 2', 'Category 3', 'Category 4'], + values: [4.3, 2.5, 3.5, 4.5], + }, + { + name: 'Series 2', + labels: ['Category 1', 'Category 2', 'Category 3', 'Category 4'], + values: [2.4, 4.4, 1.8, 2.8], + }, + { + name: 'Series 3', + labels: ['Category 1', 'Category 2', 'Category 3', 'Category 4'], + values: [2.0, 2.0, 3.0, 5.0], + }, +]; + +await pptx + .compose(async pres => { + await pres.layout('LAYOUT_4x3').addSlide(async slide => { + await slide.addChart(chart => { + chart + .template({ + pptxFile: 'path/to/template.pptx', + xmlFile: 'ppt/charts/chart1.xml', + //callback: (template, { newChartSpaceBlock, TemplateHelper, chart }) => { ... } + }) + .data(barChartData1) + .x(100) + .y(100) + .cx(400) + .cy(300) + }); + }); + }) + .save('./template-chart.pptx'); +``` + ### Images ```javascript From 44a4d4c994b00d38b44d5db94277e875beaeb327 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Fri, 26 Feb 2021 16:49:56 +0100 Subject: [PATCH 08/10] fixes heading --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 497747ad..0e1e0bf4 100644 --- a/readme.md +++ b/readme.md @@ -354,11 +354,10 @@ await pptx }) .save('./chart.pptx'); ``` +#### Chart Templates Although, you can use custom chart templates. Those will be imported from any pptx file and applied using an optional callback function. So, you don't have to wait for further chart types to be implemented, but reuse any existing chart. All you need to know is the chart-id inside the pptx file. Please refer to template-chart.test.js for further examples and handling combo chart types. -#### Bar Charts - ```javascript const PPTX = require('nodejs-pptx'); let pptx = new PPTX.Composer(); From 8ab891ce569aaf9b10a7978adeee35d6c608a893 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Mon, 1 Mar 2021 13:17:41 +0100 Subject: [PATCH 09/10] better concatentation --- lib/helpers/template-helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/helpers/template-helper.js b/lib/helpers/template-helper.js index 34a1fdbb..a864df88 100644 --- a/lib/helpers/template-helper.js +++ b/lib/helpers/template-helper.js @@ -76,12 +76,12 @@ class TemplateHelper { // We want to pick the series by chart type. static getSeriesTemplate(template, chartType) { - return template['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] + return template['c:chartSpace']['c:chart'][0]['c:plotArea'][0][`c:${chartType}`][0]['c:ser'] } // This is to replace the template series by a calculated one. static applySeriesToChart(series, newChartSpaceBlock, chartType) { - newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0]['c:'+chartType][0]['c:ser'] = series + newChartSpaceBlock['c:chartSpace']['c:chart'][0]['c:plotArea'][0][`c:${chartType}`][0]['c:ser'] = series } // We have to clone the template series get a fresh copy. From e6e6cda2b3cbb06e275db993251de3a213bef446 Mon Sep 17 00:00:00 2001 From: Thomas Singer Date: Tue, 2 Mar 2021 10:13:55 +0100 Subject: [PATCH 10/10] fix: insert multiple charts on a slide --- lib/factories/ppt/index.js | 2 +- lib/factories/ppt/slides.js | 18 ++++++++++++------ lib/helpers/ppt-factory-helper.js | 8 ++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/factories/ppt/index.js b/lib/factories/ppt/index.js index bf87acd3..d9cc452d 100644 --- a/lib/factories/ppt/index.js +++ b/lib/factories/ppt/index.js @@ -113,7 +113,7 @@ class PptFactory { } async addChart(slide, chart) { - this.slideFactory.addChartToSlideRelationship(slide, chart.name); + this.slideFactory.addChartToSlideRelationship(slide, chart); let workbookJSZip = await ExcelHelper.createWorkbook(chart.chartData); let workbookContentZipBinary = await workbookJSZip.generateAsync({ type: 'arraybuffer' }); diff --git a/lib/factories/ppt/slides.js b/lib/factories/ppt/slides.js index cc410918..3ef1b788 100644 --- a/lib/factories/ppt/slides.js +++ b/lib/factories/ppt/slides.js @@ -44,7 +44,6 @@ class SlideFactory { }; slideContent = JSON.parse(JSON.stringify(slideContent)); - this.content[slideKey] = slideContent; return slideContent; @@ -129,7 +128,8 @@ class SlideFactory { return rId; } - addChartToSlideRelationship(slide, chartName) { + addChartToSlideRelationship(slide, chart) { + let chartName = chart.name let relsKey = `ppt/slides/_rels/${slide.name}.xml.rels`; let rId = `rId${this.content[relsKey]['Relationships']['Relationship'].length + 1}`; @@ -141,7 +141,7 @@ class SlideFactory { }, }); - return rId; + chart.rId = rId } addImage(slide, image, imageObjectName, rId) { @@ -311,10 +311,10 @@ class SlideFactory { let slideKey = `ppt/slides/${slide.name}.xml`; let chartKey = `ppt/charts/${chart.name}.xml`; - let newGraphicFrameBlock = PptFactoryHelper.createBaseChartFrameBlock(chart.x(), chart.y(), chart.cx(), chart.cy()); // goes onto the slide + let newGraphicFrameBlock = PptFactoryHelper.createBaseChartFrameBlock(chart); // goes onto the slide let newChartSpaceBlock = PptFactoryHelper.createBaseChartSpaceBlock(); // goes into the chart XML - if(chart.templateParams !== undefined) { + if(chart.hasOwnProperty('templateParams')) { // If templateParams are set, a chart template will be loaded and applied to newChartSpaceBlock. await TemplateHelper.applyTemplateXml(chart.templateParams, { newChartSpaceBlock, chart }) } else { @@ -323,8 +323,14 @@ class SlideFactory { } this.content[chartKey] = newChartSpaceBlock; - this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0]['p:graphicFrame'] = newGraphicFrameBlock['p:graphicFrame']; + let tree = this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0] + if(!tree.hasOwnProperty('p:graphicFrame')) { + tree['p:graphicFrame'] = new Array + } + + tree['p:graphicFrame'].push(newGraphicFrameBlock['p:graphicFrame'][0]) + return newGraphicFrameBlock; } diff --git a/lib/helpers/ppt-factory-helper.js b/lib/helpers/ppt-factory-helper.js index f46a9aa8..f219751e 100644 --- a/lib/helpers/ppt-factory-helper.js +++ b/lib/helpers/ppt-factory-helper.js @@ -86,7 +86,7 @@ class PptFactoryHelper { // TODO: this block is taken straight from won21 (except I had to change some objects to an array of objects to support our existing // block structure); I don't like the defaults it's using and there are some slight differences from an actual PowerPoint-generated // p:graphicFrame block. Once basic charts are done, revisit this and see if this block can be made better. - static createBaseChartFrameBlock(x, y, cx, cy) { + static createBaseChartFrameBlock(chart) { return { 'p:graphicFrame': [ { @@ -119,8 +119,8 @@ class PptFactoryHelper { ], 'p:xfrm': [ { - 'a:off': [{ $: { x: x, y: y } }], - 'a:ext': [{ $: { cx: cx, cy: cy } }], + 'a:off': [{ $: { x: chart.x(), y: chart.y() } }], + 'a:ext': [{ $: { cx: chart.cx(), cy: chart.cy() } }], }, ], 'a:graphic': [ @@ -135,7 +135,7 @@ class PptFactoryHelper { $: { 'xmlns:c': 'http://schemas.openxmlformats.org/drawingml/2006/chart', 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', - 'r:id': 'rId2', + 'r:id': chart.rId, }, }, ],