Skip to content

Commit a1bb13e

Browse files
authored
Merge branch 'feature-add-media-files' into main
2 parents 7f6ca47 + 13e83ea commit a1bb13e

File tree

11 files changed

+256
-66
lines changed

11 files changed

+256
-66
lines changed

__tests__/add-media.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Automizer from '../src/automizer';
2+
3+
test('create presentation and add media 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(`EmptySlide.pptx`, 'emptySlide')
12+
.load(`SlideWithMedia.pptx`, 'media');
13+
14+
pres.addSlide('emptySlide', 1, (slide) => {
15+
slide.addElement('media', 1, 'audio');
16+
});
17+
18+
pres.addSlide('emptySlide', 1, (slide) => {
19+
slide.addElement('media', 2, 'video');
20+
slide.addElement('media', 1, 'audio');
21+
});
22+
23+
// TODO: Process related media content on added slides
24+
// pres.addSlide('media', 1, (slide) => {
25+
// slide.addElement('media', 2, 'video');
26+
// });
27+
28+
const result = await pres.write(`add-media.test.pptx`);
29+
30+
// expect(result.images).toBe(6);
31+
});

__tests__/add-svg-images.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ test('create presentation and append slides with images', async () => {
1616
slide.addElement('images', 1, 'Leaf');
1717
});
1818

19+
// TODO: Process related svg content on added slides
20+
// pres.addSlide('images', 1);
21+
1922
const result = await pres.write(`add-svg-images.test.pptx`);
2023

21-
expect(result.images).toBe(4);
24+
expect(result.images).toBe(10);
2225
});
823 KB
Binary file not shown.

src/classes/shape.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class Shape {
4141

4242
relRootTag: string;
4343
relAttribute: string;
44+
relType: string;
4445
relParent: (element: XmlElement) => XmlElement;
4546

4647
targetElement: XmlElement;
@@ -93,13 +94,15 @@ export class Shape {
9394
async setTargetElement(): Promise<void> {
9495
if (!this.sourceElement) {
9596
// If we don't have a source element, we might be trying to remove a hyperlink
96-
console.log(`Warning: No source element for shape ${this.name}. Creating empty element for operations.`);
97+
console.log(
98+
`Warning: No source element for shape ${this.name}. Creating empty element for operations.`,
99+
);
97100
if (this.shape && this.shape.mode === 'remove' && this.targetArchive) {
98101
// For remove operations, we don't need a source element
99102
// Just continue without setting targetElement
100103
return;
101104
}
102-
105+
103106
// For non-remove operations or if other conditions aren't met, throw the error
104107
console.log(this.shape);
105108
throw `No source element for shape ${this.name}`;
@@ -133,25 +136,28 @@ export class Shape {
133136
async processHyperlinks(targetSlideXml: XmlDocument): Promise<void> {
134137
// Find all text runs in the element
135138
const runs = this.targetElement.getElementsByTagName('a:r');
136-
139+
137140
for (let i = 0; i < runs.length; i++) {
138141
const run = runs[i];
139142
const rPr = run.getElementsByTagName('a:rPr')[0];
140-
143+
141144
if (rPr) {
142145
// Find hyperlink elements
143146
const hlinkClicks = rPr.getElementsByTagName('a:hlinkClick');
144-
147+
145148
for (let j = 0; j < hlinkClicks.length; j++) {
146149
const hlinkClick = hlinkClicks[j];
147150
const sourceRid = hlinkClick.getAttribute('r:id');
148-
151+
149152
if (sourceRid) {
150153
// Update the r:id attribute to use the created relationship ID
151154
hlinkClick.setAttribute('r:id', this.createdRid);
152-
155+
153156
// Ensure the xmlns:r attribute is set
154-
hlinkClick.setAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
157+
hlinkClick.setAttribute(
158+
'xmlns:r',
159+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
160+
);
155161
}
156162
}
157163
}
@@ -252,7 +258,8 @@ export class Shape {
252258

253259
async updateTargetElementRelId(): Promise<void> {
254260
this.targetElement
255-
.getElementsByTagName(this.relRootTag)[0]
261+
.getElementsByTagName(this.relRootTag)
262+
.item(0)
256263
.setAttribute(this.relAttribute, this.createdRid);
257264
}
258265

src/classes/slide.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ export class Slide extends HasShapes implements ISlide {
111111
return this;
112112
}
113113

114+
/**
115+
* The current slide will be fully calculated, but removed from slide
116+
* sortation.
117+
*/
118+
drop() {}
119+
114120
/**
115121
* Find another slide layout by name.
116122
* @param targetLayoutName

src/constants/constants.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,61 @@ import {
44
TrackedRelationTag,
55
} from '../types/types';
66

7-
export const TargetByRelIdMap = {
7+
export const TargetByRelIdMap: Record<string, TargetByRelIdMapParam> = {
88
chart: {
99
relRootTag: 'c:chart',
1010
relAttribute: 'r:id',
1111
prefix: '../charts/chart',
12-
} as TargetByRelIdMapParam,
12+
},
1313
chartEx: {
1414
relRootTag: 'cx:chart',
1515
relAttribute: 'r:id',
1616
prefix: '../charts/chartEx',
17-
} as TargetByRelIdMapParam,
17+
},
1818
image: {
1919
relRootTag: 'a:blip',
2020
relAttribute: 'r:embed',
2121
prefix: '../media/image',
22-
} as TargetByRelIdMapParam,
22+
},
2323
'image:svg': {
2424
relRootTag: 'asvg:svgBlip',
2525
relAttribute: 'r:embed',
2626
prefix: '../media/image',
27-
} as TargetByRelIdMapParam,
27+
},
28+
'image:media': {
29+
relRootTag: 'p14:media',
30+
relAttribute: 'r:embed',
31+
relType: 'http://schemas.microsoft.com/office/2007/relationships/media',
32+
prefix: '../media/media',
33+
findAll: true,
34+
},
35+
'image:audioFile': {
36+
relRootTag: 'a:audioFile',
37+
relAttribute: 'r:link',
38+
prefix: '../media/media',
39+
relType:
40+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio',
41+
findAll: true,
42+
},
43+
'image:videoFile': {
44+
relRootTag: 'a:videoFile',
45+
relAttribute: 'r:link',
46+
prefix: '../media/media',
47+
relType:
48+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/video',
49+
findAll: true,
50+
},
2851
hyperlink: {
2952
relRootTag: 'a:hlinkClick',
3053
relAttribute: 'r:id',
3154
prefix: '',
3255
findAll: true,
33-
} as TargetByRelIdMapParam,
56+
},
3457
oleObject: {
3558
relRootTag: 'p:oleObj',
3659
relAttribute: 'r:id',
3760
prefix: '../embeddings/oleObject',
38-
} as TargetByRelIdMapParam,
61+
},
3962
};
4063

4164
export const imagesTrack: () => TrackedRelation[] = () => [

src/dev.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,26 @@ const run = async () => {
77
const automizer = new Automizer({
88
templateDir,
99
outputDir,
10-
autoImportSlideMasters: true,
11-
cleanupPlaceholders: true,
1210
});
1311

1412
let pres = automizer
1513
.loadRoot(`RootTemplate.pptx`)
16-
.load(`EmptySlidePlaceholders.pptx`, 'placeholder')
17-
.load(`EmptySlide.pptx`, 'emptySlide');
14+
.load(`EmptySlide.pptx`, 'emptySlide')
15+
.load(`SlideWithMedia.pptx`, 'media');
1816

19-
pres.addSlide('emptySlide', 1, (slide) => {
20-
slide.addElement('placeholder', 1, 'Titel 4');
17+
// pres.addSlide('emptySlide', 1, (slide) => {
18+
// slide.addElement('media', 1, 'audio');
19+
// });
20+
//
21+
// pres.addSlide('emptySlide', 1, (slide) => {
22+
// slide.addElement('media', 2, 'video');
23+
// });
24+
25+
pres.addSlide('media', 1, (slide) => {
26+
//slide.addElement('media', 2, 'video');
2127
});
2228

23-
pres.write(`myOutputPresentation.pptx`).then((summary) => {
29+
pres.write(`testMedia.pptx`).then((summary) => {
2430
console.log(summary);
2531
});
2632
};

src/enums/content-type-map.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export enum ContentTypeMap {
44
png = 'image/png',
55
gif = 'image/gif',
66
svg = 'image/svg+xml',
7+
mp3 = 'audio/mp3',
78
m4v = 'video/mp4',
89
mp4 = 'video/mp4',
910
emf = 'image/x-emf',

src/helper/xml-helper.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ export class XmlHelper {
285285
const relationshipItems = xml.getElementsByTagName(tag);
286286

287287
const rels = [];
288+
288289
for (const i in relationshipItems) {
289290
if (relationshipItems[i].getAttribute) {
290291
cb(relationshipItems[i], rels);
@@ -353,15 +354,15 @@ export class XmlHelper {
353354
type: string,
354355
): Promise<Target> {
355356
const params = TargetByRelIdMap[type];
356-
357+
357358
// For elements that need to search all instances (like hyperlinks)
358359
if (params.findAll) {
359360
// Find all hyperlink elements
360361
const hyperlinks = element.getElementsByTagName(params.relRootTag);
361362
if (hyperlinks.length > 0) {
362363
// Use the first hyperlink found
363364
const sourceRid = hyperlinks[0].getAttribute(params.relAttribute);
364-
365+
365366
// Get all relationships
366367
const allRels = await XmlHelper.getRelationshipItems(
367368
archive,
@@ -375,25 +376,34 @@ export class XmlHelper {
375376
element: element,
376377
isExternal: element.getAttribute('TargetMode') === 'External',
377378
} as Target);
378-
}
379+
},
379380
);
380-
381+
381382
// Find the matching relationship
382383
const target = allRels.find((rel) => rel.rId === sourceRid);
383384
return target;
384385
}
385386
} else {
386387
// Standard behavior for other element types
387388
const sourceRid = element
388-
.getElementsByTagName(params.relRootTag)[0]
389-
.getAttribute(params.relAttribute);
389+
.getElementsByTagName(params.relRootTag)
390+
.item(0)
391+
?.getAttribute(params.relAttribute);
392+
393+
if (!sourceRid) {
394+
throw 'No sourceRid for ' + params.relRootTag;
395+
}
390396

391397
const shapeRels = await XmlHelper.getRelationshipTargetsByPrefix(
392398
archive,
393399
relsPath,
394400
params.prefix,
395401
);
396-
const target = shapeRels.find((rel) => rel.rId === sourceRid);
402+
403+
const target = shapeRels.find((rel) => {
404+
return rel.rId === sourceRid;
405+
});
406+
397407
return target;
398408
}
399409
}

0 commit comments

Comments
 (0)