Skip to content

Commit 728cf3c

Browse files
committed
feat(diagram): basic handler for diagrams (aka smartart)
1 parent 469c4a9 commit 728cf3c

File tree

14 files changed

+382
-48
lines changed

14 files changed

+382
-48
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Automizer from '../src/automizer';
2+
3+
test('create presentation and append diagrams', async () => {
4+
const automizer = new Automizer({
5+
templateDir: `${__dirname}/pptx-templates`,
6+
outputDir: `${__dirname}/pptx-output`,
7+
});
8+
9+
const pres = automizer
10+
.loadRoot(`RootTemplate.pptx`)
11+
.load(`EmptySlide.pptx`, 'empty')
12+
.load(`SlideWithDiagrams.pptx`, 'diagrams');
13+
14+
pres.addSlide('diagrams', 1);
15+
pres.addSlide('diagrams', 2);
16+
17+
pres.addSlide('empty', 1, (slide) => {
18+
slide.addElement('diagrams', 1, 'MatrixDiagram')
19+
});
20+
21+
const result = await pres.write(`add-slide-diagrams.test.pptx`);
22+
23+
expect(result.slides).toBe(4);
24+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Automizer, { modify } from '../src/index';
2+
3+
test('modify of grouped shapes', async () => {
4+
const automizer = new Automizer({
5+
templateDir: `${__dirname}/pptx-templates`,
6+
outputDir: `${__dirname}/pptx-output`,
7+
});
8+
9+
const pres = automizer
10+
.loadRoot(`RootTemplate.pptx`)
11+
.load(`SlideWithShapes.pptx`, 'shapes');
12+
13+
const result = await pres
14+
.addSlide('shapes', 3, (slide) => {
15+
// slide.modifyElement('Arrow', modify.setText('stays in group'));
16+
slide.addElement('shapes', 3, 'Arrow', modify.setText('stays in group'));
17+
})
18+
.write(`modify-shapes-group.test.pptx`);
19+
20+
expect(result.slides).toBe(2);
21+
});

src/classes/has-shapes.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { OLEObject } from '../shapes/ole';
4040
import { Hyperlink } from '../shapes/hyperlink';
4141
import { HyperlinkProcessor } from '../helper/hyperlink-processor';
4242
import { vd } from '../helper/general-helper';
43+
import { Diagram } from '../shapes/diagram';
4344

4445
export default class HasShapes {
4546
/**
@@ -125,6 +126,8 @@ export default class HasShapes {
125126
*/
126127
unsupportedTags = [
127128
'p:custDataLst',
129+
// exclude bullet images
130+
// 'a:buBlip',
128131
// 'p:oleObj',
129132
// 'mc:AlternateContent',
130133
// 'a14:imgProps',
@@ -888,6 +891,19 @@ export default class HasShapes {
888891
).modifyOnAddedSlide(this.targetTemplate, this.targetNumber);
889892
}
890893

894+
const diagrams = await Diagram.getAllOnSlide(this.sourceArchive, this.relsPath);
895+
for (const diagram of diagrams) {
896+
await new Diagram(
897+
{
898+
mode: 'append',
899+
target: diagram,
900+
sourceArchive: this.sourceArchive,
901+
sourceSlideNumber: this.sourceNumber,
902+
},
903+
this.targetType,
904+
).modifyOnAddedSlide(this.targetTemplate, this.targetNumber);
905+
}
906+
891907
const oleObjects = await OLEObject.getAllOnSlide(
892908
this.sourceArchive,
893909
this.relsPath,
@@ -994,6 +1010,19 @@ export default class HasShapes {
9941010
} as AnalyzedElementType;
9951011
}
9961012

1013+
const isDiagram = sourceElement.getElementsByTagName('p:nvGraphicFramePr');
1014+
if (isDiagram.length) {
1015+
return {
1016+
type: ElementType.Diagram,
1017+
target: await XmlHelper.getTargetByRelId(
1018+
sourceArchive,
1019+
relsPath,
1020+
sourceElement,
1021+
'diagram',
1022+
),
1023+
} as AnalyzedElementType;
1024+
}
1025+
9971026
const isOLEObject = sourceElement.getElementsByTagName('p:oleObj');
9981027
if (isOLEObject.length) {
9991028
const target = await XmlHelper.getTargetByRelId(

src/classes/shape.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { XmlHelper } from '../helper/xml-helper';
2-
import { GeneralHelper } from '../helper/general-helper';
2+
import { GeneralHelper, vd } from '../helper/general-helper';
33
import { HyperlinkProcessor } from '../helper/hyperlink-processor';
44
import {
55
ChartModificationCallback,
@@ -172,20 +172,26 @@ export class Shape {
172172
XmlHelper.writeXmlToArchive(archive, slideFile, targetSlideXml);
173173
}
174174

175-
async updateElementsRelId(): Promise<void> {
175+
async updateElementsRelId(cb?: (targetElement: XmlElement) => void): Promise<void> {
176176
const targetSlideXml = await XmlHelper.getXmlFromArchive(
177177
this.targetArchive,
178178
this.targetSlideFile,
179179
);
180+
181+
180182
const targetElements = await this.getElementsByRid(
181183
targetSlideXml,
182184
this.sourceRid,
183185
);
184186

185187
targetElements.forEach((targetElement: XmlElement) => {
186-
this.relParent(targetElement)
187-
.getElementsByTagName(this.relRootTag)[0]
188-
.setAttribute(this.relAttribute, this.createdRid);
188+
if(cb && typeof cb === 'function') {
189+
cb(targetElement)
190+
} else {
191+
this.relParent(targetElement)
192+
.getElementsByTagName(this.relRootTag)[0]
193+
.setAttribute(this.relAttribute, this.createdRid);
194+
}
189195
});
190196

191197
XmlHelper.writeXmlToArchive(

src/classes/slide.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export class Slide extends HasShapes implements ISlide {
233233
const elementsToProcess = [...unmatchedElements];
234234

235235
elementsToProcess.forEach((element) => {
236-
const bestAlternativeMatch = this.findBestAlternativeMatch(
236+
const bestAlternativeMatch = XmlPlaceholderHelper.findBestTargetPlaceholderAlternative(
237237
element,
238238
targetPlaceholders,
239239
usedPlaceholders,
@@ -250,27 +250,6 @@ export class Slide extends HasShapes implements ISlide {
250250
});
251251
}
252252

253-
/**
254-
* Finds the best alternative placeholder match for an unmatched element.
255-
*
256-
* @param element - Element that needs a placeholder match
257-
* @param targetPlaceholders - Available placeholders in target layout
258-
* @param usedPlaceholders - Already assigned placeholders
259-
* @returns PlaceholderInfo | null - Best match or null if none found
260-
* @private
261-
*/
262-
private findBestAlternativeMatch(
263-
element: ElementInfo,
264-
targetPlaceholders: PlaceholderInfo[],
265-
usedPlaceholders: PlaceholderInfo[],
266-
): PlaceholderInfo | null {
267-
return XmlPlaceholderHelper.findBestTargetPlaceholderAlternative(
268-
element,
269-
targetPlaceholders,
270-
usedPlaceholders,
271-
);
272-
}
273-
274253
/**
275254
* Applies an alternative placeholder match to an element.
276255
*

src/classes/template.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class Template implements ITemplate {
9898
new CountHelper('slides', newTemplate),
9999
new CountHelper('charts', newTemplate),
100100
new CountHelper('images', newTemplate),
101+
new CountHelper('diagrams', newTemplate),
101102
new CountHelper('masters', newTemplate),
102103
new CountHelper('layouts', newTemplate),
103104
new CountHelper('themes', newTemplate),

src/constants/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export const TargetByRelIdMap: Record<string, TargetByRelIdMapParam> = {
1515
relAttribute: 'r:id',
1616
prefix: '../charts/chartEx',
1717
},
18+
diagram: {
19+
relRootTag: 'dgm:relIds',
20+
relAttribute: 'r:dm',
21+
prefix: '../diagrams/data',
22+
},
1823
image: {
1924
relRootTag: 'a:blip',
2025
relAttribute: 'r:embed',

src/dev.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,20 @@ const run = async () => {
66
const outputDir = `${__dirname}/../__tests__/pptx-output`;
77
const templateDir = `${__dirname}/../__tests__/pptx-templates`;
88

9-
// Step 1: Create a pptx with images and a chart inside.
10-
// The chart is modified by pptx-automizer
11-
129
const automizer = new Automizer({
1310
templateDir,
1411
outputDir,
1512
verbosity: 2,
1613
removeExistingSlides: true,
1714
});
1815

19-
const pres = automizer.loadRoot(`RootTemplate.pptx`).load(`SlideWithShapes.pptx`);
16+
const pres = automizer.loadRoot(`RootTemplate.pptx`).load(`SlideWithDiagram.pptx`);
2017

21-
pres.addSlide("SlideWithShapes.pptx", 3, async (slide) => {
22-
const infoTopLevel = await slide.getElement('TopLevelGroup')
23-
const groupInfoTopLevel = infoTopLevel.getGroupInfo()
18+
pres.addSlide("SlideWithDiagram.pptx", 1, async (slide) => {
2419

2520
})
2621

27-
await pres.write(`modify-multi-text.test.pptx`);
22+
await pres.write(`add-diagram.test.pptx`);
2823
};
2924

3025
run().catch((error) => {

src/enums/element-type.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
export enum ElementType {
22
Chart = 'Chart',
33
Image = 'Image',
4+
Diagram = 'Diagram',
45
Shape = 'Generic',
5-
OLEObject = 'OLEObject',
6+
OLEObject = 'OLEObject',
67
Hyperlink = 'Hyperlink',
78
}
89

910
export enum ElementSubtype {
1011
chart = 'chart',
1112
chartEx = 'chartEx',
12-
oleObject = 'oleObject',
13+
oleObject = 'oleObject',
1314
hyperlink = 'hyperlink',
1415
}

src/helper/count-helper.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ export class CountHelper implements ICounter {
6363
return CountHelper.countCharts(presentation);
6464
case 'images':
6565
return CountHelper.countImages(presentation);
66-
case 'oleObjects':
67-
return CountHelper.countOleObjects(presentation);
68-
66+
case 'diagrams':
67+
return CountHelper.countDiagrams(presentation);
68+
case 'oleObjects':
69+
return CountHelper.countOleObjects(presentation);
6970
}
7071

7172
throw new Error(`No way to count ${this.name}.`);
@@ -138,23 +139,24 @@ export class CountHelper implements ICounter {
138139
).length;
139140
}
140141

141-
private static async countOleObjects(presentation: IArchive): Promise<number> {
142+
private static async countOleObjects(
143+
presentation: IArchive,
144+
): Promise<number> {
142145
const contentTypesXml = await XmlHelper.getXmlFromArchive(
143146
presentation,
144147
'[Content_Types].xml',
145148
);
146149
const overrides = contentTypesXml.getElementsByTagName('Override');
147-
150+
148151
return Object.keys(overrides)
149152
.map((key) => overrides[key] as XmlElement)
150153
.filter(
151154
(o) =>
152155
o.getAttribute &&
153156
o.getAttribute('ContentType') ===
154-
`application/vnd.openxmlformats-officedocument.oleObject`
157+
`application/vnd.openxmlformats-officedocument.oleObject`,
155158
).length;
156159
}
157-
158160

159161
private static async countImages(presentation: IArchive): Promise<number> {
160162
const mediaFiles = await presentation.folder('ppt/media');
@@ -163,4 +165,21 @@ export class CountHelper implements ICounter {
163165
).length;
164166
return count;
165167
}
168+
169+
private static async countDiagrams(presentation: IArchive): Promise<number> {
170+
const contentTypesXml = await XmlHelper.getXmlFromArchive(
171+
presentation,
172+
'[Content_Types].xml',
173+
);
174+
const overrides = contentTypesXml.getElementsByTagName('Override');
175+
176+
return Object.keys(overrides)
177+
.map((key) => overrides[key] as XmlElement)
178+
.filter(
179+
(o) =>
180+
o.getAttribute &&
181+
o.getAttribute('ContentType') ===
182+
`application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml`,
183+
).length;
184+
}
166185
}

0 commit comments

Comments
 (0)