Skip to content

Commit 8d59e05

Browse files
authored
Merge pull request #92 from singerla/main
Sync with main
2 parents 01a49ca + f3b17ab commit 8d59e05

File tree

10 files changed

+173
-60
lines changed

10 files changed

+173
-60
lines changed

README.md

Lines changed: 102 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,43 @@ Thanks to all contributors! You are always welcome to share code, tipps and idea
1111
If you require commercial support for complex .pptx automation, you can explore [ensemblio.com](https://ensemblio.com). Ensemblio is a web application that leverages `pptx-automizer` and `automizer-data` to provide an accessible and convenient solution for automating .pptx files. Engaging with Ensemblio is likely to enhance and further develop this library.
1212

1313
## Table of contents
14+
1415
<!-- TOC -->
15-
* [Requirements and Limitations](#requirements-and-limitations)
16-
* [Shape Types](#shape-types)
17-
* [Chart Types](#chart-types)
18-
* [Animations](#animations)
19-
* [Slide Masters and -Layouts](#slide-masters-and--layouts)
20-
* [Direct Manipulation of Elements](#direct-manipulation-of-elements)
21-
* [PowerPoint Version](#powerpoint-version)
22-
* [Installation](#installation)
23-
* [As a Cloned Repository](#as-a-cloned-repository)
24-
* [As a Package](#as-a-package)
25-
* [Usage](#usage)
26-
* [Basic Example](#basic-example)
27-
* [How to Select Slides Shapes](#how-to-select-slides-shapes)
28-
* [Select slide by number and shape by name](#select-slide-by-number-and-shape-by-name)
29-
* [Select slides by creationId](#select-slides-by-creationid)
30-
* [Find and Modify Shapes](#find-and-modify-shapes)
31-
* [Modify Text](#modify-text)
32-
* [Modify Images](#modify-images)
33-
* [Modify Tables](#modify-tables)
34-
* [Modify Charts](#modify-charts)
35-
* [Modify Extended Charts](#modify-extended-charts)
36-
* [Remove elements from a slide](#remove-elements-from-a-slide)
37-
* [Tipps and Tricks](#tipps-and-tricks)
38-
* [Loop through the slides of a presentation](#loop-through-the-slides-of-a-presentation)
39-
* [Quickly get all slide numbers of a template](#quickly-get-all-slide-numbers-of-a-template)
40-
* [Find all text elements on a slide](#find-all-text-elements-on-a-slide)
41-
* [Sort output slides](#sort-output-slides)
42-
* [Import and modify slide Masters](#import-and-modify-slide-masters)
43-
* [Track status of automation process](#track-status-of-automation-process)
44-
* [More examples](#more-examples)
45-
* [Testing](#testing)
46-
* [Special Thanks](#special-thanks)
47-
* [Commercial Support](#commercial-support)
16+
17+
- [Requirements and Limitations](#requirements-and-limitations)
18+
- [Shape Types](#shape-types)
19+
- [Chart Types](#chart-types)
20+
- [Animations](#animations)
21+
- [Slide Masters and -Layouts](#slide-masters-and--layouts)
22+
- [Direct Manipulation of Elements](#direct-manipulation-of-elements)
23+
- [PowerPoint Version](#powerpoint-version)
24+
- [Installation](#installation)
25+
- [As a Cloned Repository](#as-a-cloned-repository)
26+
- [As a Package](#as-a-package)
27+
- [Usage](#usage)
28+
- [Basic Example](#basic-example)
29+
- [How to Select Slides Shapes](#how-to-select-slides-shapes)
30+
- [Select slide by number and shape by name](#select-slide-by-number-and-shape-by-name)
31+
- [Select slides by creationId](#select-slides-by-creationid)
32+
- [Find and Modify Shapes](#find-and-modify-shapes)
33+
- [Modify Text](#modify-text)
34+
- [Modify Images](#modify-images)
35+
- [Modify Tables](#modify-tables)
36+
- [Modify Charts](#modify-charts)
37+
- [Modify Extended Charts](#modify-extended-charts)
38+
- [Remove elements from a slide](#remove-elements-from-a-slide)
39+
- [Tipps and Tricks](#tipps-and-tricks)
40+
- [Loop through the slides of a presentation](#loop-through-the-slides-of-a-presentation)
41+
- [Quickly get all slide numbers of a template](#quickly-get-all-slide-numbers-of-a-template)
42+
- [Find all text elements on a slide](#find-all-text-elements-on-a-slide)
43+
- [Sort output slides](#sort-output-slides)
44+
- [Import and modify slide Masters](#import-and-modify-slide-masters)
45+
- [Track status of automation process](#track-status-of-automation-process)
46+
- [More examples](#more-examples)
47+
- [Create a new modifier](#create-a-new-modifier)
48+
- [Troubleshooting](#troubleshooting)
49+
- [Testing](#testing)
50+
- [Special Thanks](#special-thanks)
4851
<!-- TOC -->
4952

5053
# Requirements and Limitations
@@ -475,6 +478,7 @@ Find more examples on image manipulation:
475478

476479
- [Add external image](https://github.com/singerla/pptx-automizer/blob/main/__tests__/add-external-image.test.ts)
477480
- [Modify duotone color overlay for images](https://github.com/singerla/pptx-automizer/blob/main/__tests__/modify-image-duotone.test.ts)
481+
- [Swap image source on a slide master](https://github.com/singerla/pptx-automizer/blob/main/__tests__/modify-master-external-image.test.ts)
478482

479483
## Modify Tables
480484

@@ -586,7 +590,7 @@ If you would like to modify elements in a single .pptx file, it is important to
586590

587591
This is how it works internally:
588592

589-
- Load a root template to append slides to
593+
- Load a root template to append slides to it
590594
- (Probably) load root template again to modify slides
591595
- Load other templates
592596
- Append a loaded slide to (probably truncated) root template
@@ -618,7 +622,7 @@ const run = async () => {
618622
// Defining a "name" as second params makes it a little easier
619623
.load(`SlideWithShapes.pptx`, 'myTemplate');
620624

621-
// This is brandnew: get useful information about loaded templates:
625+
// Get useful information about loaded templates:
622626
const myTemplates = await pres.getInfo();
623627
const mySlides = myTemplates.slidesByTemplate(`myTemplate`);
624628

@@ -853,6 +857,55 @@ const automizer = new Automizer({
853857
});
854858
```
855859

860+
## Create a new modifier
861+
862+
If the built-in modifiers of `pptx-automizer` are not sufficient to your task, you can access the target xml elements with [xmldom](https://github.com/xmldom/xmldom). A modifier is a wrapper for such an operation.
863+
864+
Let's first take a look at a (simplified) existing modifier: `ModifyTextHelper.content('This is my text')`.
865+
866+
```ts
867+
// "setTextContent" is a function that returns a function.
868+
// A "label" argument needs to be passed to "setTextContent".
869+
const setTextContent = function (label: number | string) {
870+
// On setup, we can handle the argument.
871+
const newTextContent = String(label);
872+
873+
// A new function is returned to apply the label at runtime.
874+
return function (shape: XmlElement) {
875+
// "shape" contains a modifiable xmldom object.
876+
// You can use a selector to find the required 'a:t' element:
877+
const textElement = shape.getElementsByTagName('a:t').item(0);
878+
879+
// You can now apply the "newTextContent".
880+
if (textElement?.firstChild) {
881+
// Refer to xmldom for available functions.
882+
textElement.firstChild.textContent = newTextContent;
883+
}
884+
// It is possible to output the xml to console at any time.
885+
// XmlHelper.dump(element);
886+
};
887+
};
888+
```
889+
This function will construct an anonymous callback function on setup, while the callback function itself will be executed on runtime, when it's up to the target element on a slide.
890+
891+
You can use the modifier e.g. on adding an element:
892+
893+
```ts
894+
pres.addSlide('SlideWithShapes.pptx', 2, (slide) => {
895+
// This will import the 'Drum' shape
896+
slide.modifyElement('Cloud', [
897+
// 1. Dump the original xml:
898+
// Notice: don't call XmlHelper.dump, just pass it
899+
XmlHelper.dump,
900+
// 2. Apply modifier from the example above:
901+
setTextContent('New text'),
902+
XmlHelper.dump,
903+
]);
904+
});
905+
```
906+
907+
We can wrap any xml modification by such a modifier. If you have a working example and you think it will be useful to others, you are very welcome to fork this repo and send a pull request or simply [post it](https://github.com/singerla/pptx-automizer/issues/new).
908+
856909
## More examples
857910

858911
Take a look into [**tests**-directory](https://github.com/singerla/pptx-automizer/blob/main/__tests__) to see a lot of examples for several use cases, e.g.:
@@ -864,6 +917,20 @@ Take a look into [**tests**-directory](https://github.com/singerla/pptx-automize
864917
- [Update chart plot area coordinates](https://github.com/singerla/pptx-automizer/blob/main/__tests__/modify-chart-plot-area.test.ts)
865918
- [Update chart legend](https://github.com/singerla/pptx-automizer/blob/main/__tests__/modify-chart-legend.test.ts)
866919

920+
## Troubleshooting
921+
922+
If you encounter problems when opening a `.pptx`-file modified by this library, you might worry about PowerPoint not giving any details about the error. It can be hard to find the cause, but there are some things you can check:
923+
924+
- **Broken relation**: There are still unsupported shape types and `pptx-automizer` wil not copy required relations of those. You can inflate `.pptx`-output and check `ppt/slides/_rels/slide[#].xml.rels`-files to find possible missing files.
925+
- **Unsupported media**: You can also take a look at the `ppt/media`-directory of an inflated `.pptx`-file. If you discover any unusual file formats, remove or replace the files by one of the [known types](https://github.com/singerla/pptx-automizer/blob/main/src/enums/content-type-map.ts).
926+
- **Broken animation**: Pay attention to modified/removed shapes which are part of an animation. In case of doubt, (temporarily) remove all animations from your template. (see [#78](https://github.com/singerla/pptx-automizer/issues/78))
927+
- **Proprietary/Binary contents** (e.g. ThinkCell): Walk through all slides, slideMasters and slideLayouts and seek for hidden Objects. Hit `ALT+F10` to toggle the sidebar.
928+
- **Chart styles not working**: If you try to change e.g. color or size of a chart data label, and it doesn't work as expected, try to remove all data labels and activate them again. If this does not help, try to give the first data label of a series a slightly different style (this creates a single data point).
929+
- **Replace Text not working**: Cut out your e.g. {CustomerName} tag from textbox to clipboard, paste it into a plaintext editor to remove all (visible and invisible) formatting. Copy & paste {CustomerName} back to the textbox. (see [#82](https://github.com/singerla/pptx-automizer/issues/82) and [#73](https://github.com/singerla/pptx-automizer/issues/73))
930+
931+
932+
If none of these could help, please don't hesitate to [talk about it](https://github.com/singerla/pptx-automizer/issues/new).
933+
867934
## Testing
868935

869936
You can run all unit tests using these commands:
@@ -881,8 +948,3 @@ This project was inspired by:
881948
- [officegen](https://github.com/Ziv-Barber/officegen)
882949
- [node-pptx](https://github.com/heavysixer/node-pptx)
883950
- [docxtemplater](https://github.com/open-xml-templating/docxtemplater)
884-
885-
# Commercial Support
886-
887-
If you need commercial support on complex .pptx automation, please take a look at [ensemblio.com](https://ensemblio.com).
888-
![ensemblio](https://ensemblio.com/ensemblio-lg.png)

__tests__/add-external-image.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ test('Load external media, add/modify image and set image target', async () => {
1414
const pres = automizer
1515
.loadRoot(`RootTemplate.pptx`)
1616
.loadMedia([`feather.png`, `test.png`])
17+
.loadMedia(`test.png`, `${__dirname}/../__tests__/media`, 'pre_')
1718
.load(`SlideWithShapes.pptx`, 'shapes')
1819
.load(`SlideWithImages.pptx`, 'images');
1920

@@ -29,7 +30,7 @@ test('Load external media, add/modify image and set image target', async () => {
2930

3031
pres.addSlide('images', 1, (slide) => {
3132
slide.modifyElement('Grafik 5', [
32-
ModifyImageHelper.setRelationTarget('test.png'),
33+
ModifyImageHelper.setRelationTarget('pre_test.png'),
3334
]);
3435
});
3536

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Automizer from '../src/automizer';
2+
import { ModifyImageHelper, ModifyShapeHelper } from '../src';
3+
import { CmToDxa } from '../src/helper/modify-helper';
4+
5+
test('Load external media, modify image target on slide master', async () => {
6+
const automizer = new Automizer({
7+
templateDir: `${__dirname}/../__tests__/pptx-templates`,
8+
outputDir: `${__dirname}/../__tests__/pptx-output`,
9+
mediaDir: `${__dirname}/../__tests__/images`,
10+
removeExistingSlides: true,
11+
cleanup: true,
12+
});
13+
14+
const pres = automizer
15+
.loadRoot(`RootTemplateWithImages.pptx`)
16+
.loadMedia([`test.jpg`])
17+
.load(`RootTemplateWithImages.pptx`, 'base');
18+
19+
pres.addMaster('base', 1, (master) => {
20+
master.modifyElement('masterImagePNG', [
21+
ModifyImageHelper.setRelationTarget('test.jpg'),
22+
]);
23+
});
24+
25+
// Expect imported slide master (#2) to have swapped (left top) background image
26+
const result = await pres.write(`modify-master-add-external-image.test.pptx`);
27+
28+
expect(result.images).toBe(8);
29+
});
291 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pptx-automizer",
3-
"version": "0.4.6",
3+
"version": "0.4.7",
44
"description": "A template based pptx generator",
55
"repository": {
66
"type": "git",

src/automizer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ export default class Automizer implements IPresentationProps {
205205
* @param filename Filename or path to the media file.
206206
* @param dir Specify custom path for media instead of mediaDir from AutomizerParams.
207207
*/
208-
public loadMedia(filename: string | string[], dir?: string): this {
208+
public loadMedia(
209+
filename: string | string[],
210+
dir?: string,
211+
prefix?: string,
212+
): this {
209213
const files = GeneralHelper.arrayify(filename);
210214
if (!this.rootTemplate) {
211215
throw "Can't load media, you need to load a root template first";
@@ -226,6 +230,7 @@ export default class Automizer implements IPresentationProps {
226230
directory,
227231
filepath,
228232
extension,
233+
prefix,
229234
});
230235
});
231236
return this;
@@ -482,8 +487,12 @@ export default class Automizer implements IPresentationProps {
482487
*/
483488
public async writeMediaFiles(): Promise<void> {
484489
for (const file of this.rootTemplate.mediaFiles) {
485-
const archiveFilename = 'ppt/media/' + file.file;
486490
const data = fs.readFileSync(file.filepath);
491+
let archiveFilename = 'ppt/media/' + file.file;
492+
if (file.prefix) {
493+
archiveFilename = 'ppt/media/' + file.prefix + file.file;
494+
}
495+
487496
await this.rootTemplate.archive.write(archiveFilename, data);
488497
await XmlHelper.appendImageExtensionToContentType(
489498
this.rootTemplate.archive,

src/classes/has-shapes.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Image } from '../shapes/image';
3232
import { ElementType } from '../enums/element-type';
3333
import { GenericShape } from '../shapes/generic';
3434
import { XmlSlideHelper } from '../helper/xml-slide-helper';
35+
import { vd } from '../helper/general-helper';
3536

3637
export default class HasShapes {
3738
/**
@@ -387,17 +388,27 @@ export default class HasShapes {
387388
? this.getSlideNumber(template, importElement.slideNumber)
388389
: importElement.slideNumber;
389390

390-
let sourcePath = `ppt/slides/slide${slideNumber}.xml`;
391-
391+
let currentMode = 'slideToSlide';
392392
if (this.targetType === 'slideMaster') {
393-
// It is possible to import shapes from loaded presentations,
394-
// as well as to modify an existing shape on current slideMaster
395-
sourcePath =
396-
importElement.mode === 'append'
397-
? `ppt/slides/slide${slideNumber}.xml`
398-
: `ppt/slideMasters/slideMaster${slideNumber}.xml`;
393+
if (importElement.mode === 'append') {
394+
currentMode = 'slideToMaster';
395+
} else {
396+
currentMode = 'onMaster';
397+
}
399398
}
400399

400+
// It is possible to import shapes from loaded slides to slideMaster,
401+
// as well as to modify an existing shape on current slideMaster
402+
const sourcePath =
403+
currentMode === 'onMaster'
404+
? `ppt/slideMasters/slideMaster${slideNumber}.xml`
405+
: `ppt/slides/slide${slideNumber}.xml`;
406+
407+
const sourceRelPath =
408+
currentMode === 'onMaster'
409+
? `ppt/slideMasters/_rels/slideMaster${slideNumber}.xml.rels`
410+
: `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
411+
401412
const sourceArchive = await template.archive;
402413
const useCreationIds =
403414
template.useCreationIds === true && template.creationIds !== undefined;
@@ -420,7 +431,7 @@ export default class HasShapes {
420431
const appendElementParams = await this.analyzeElement(
421432
sourceElement,
422433
sourceArchive,
423-
slideNumber,
434+
sourceRelPath,
424435
);
425436

426437
return {
@@ -754,13 +765,13 @@ export default class HasShapes {
754765
async analyzeElement(
755766
sourceElement: XmlElement,
756767
sourceArchive: IArchive,
757-
slideNumber: number,
768+
relsPath: string,
758769
): Promise<AnalyzedElementType> {
759770
const isChart = sourceElement.getElementsByTagName('c:chart');
760771
if (isChart.length) {
761772
const target = await XmlHelper.getTargetByRelId(
762773
sourceArchive,
763-
slideNumber,
774+
relsPath,
764775
sourceElement,
765776
'chart',
766777
);
@@ -775,7 +786,7 @@ export default class HasShapes {
775786
if (isChartEx.length) {
776787
const target = await XmlHelper.getTargetByRelId(
777788
sourceArchive,
778-
slideNumber,
789+
relsPath,
779790
sourceElement,
780791
'chartEx',
781792
);
@@ -792,7 +803,7 @@ export default class HasShapes {
792803
type: ElementType.Image,
793804
target: await XmlHelper.getTargetByRelId(
794805
sourceArchive,
795-
slideNumber,
806+
relsPath,
796807
sourceElement,
797808
'image',
798809
),

src/helper/xml-helper.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,14 @@ export class XmlHelper {
331331

332332
static async getTargetByRelId(
333333
archive: IArchive,
334-
slideNumber: number,
334+
relsPath: string,
335335
element: XmlElement,
336336
type: string,
337337
): Promise<Target> {
338338
const params = TargetByRelIdMap[type];
339339
const sourceRid = element
340340
.getElementsByTagName(params.relRootTag)[0]
341341
.getAttribute(params.relAttribute);
342-
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
343342
const imageRels = await XmlHelper.getRelationshipTargetsByPrefix(
344343
archive,
345344
relsPath,

src/shapes/image.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ export class Image extends Shape implements IImage {
8282
this.applyImageCallbacks();
8383

8484
if (this.hasSvgRelation()) {
85+
const relsPath = `ppt/slides/_rels/slide${this.sourceSlideNumber}.xml.rels`;
8586
const target = await XmlHelper.getTargetByRelId(
8687
this.sourceArchive,
87-
this.sourceSlideNumber,
88+
relsPath,
8889
this.targetElement,
8990
'image:svg',
9091
);

0 commit comments

Comments
 (0)