Skip to content

Commit dac9596

Browse files
authored
Merge pull request #31 from singerla/feature-modify-presentation
Feature modify presentation
2 parents 1ac8502 + c6ae6dd commit dac9596

File tree

9 files changed

+189
-27
lines changed

9 files changed

+189
-27
lines changed

__tests__/sort-slides.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Automizer from '../src/automizer';
2+
import ModifyPresentationHelper from '../src/helper/modify-presentation-helper';
3+
4+
test('read root presentation and sort slides', async () => {
5+
const automizer = new Automizer({
6+
templateDir: `${__dirname}/pptx-templates`,
7+
outputDir: `${__dirname}/pptx-output`,
8+
});
9+
const pres = automizer
10+
.loadRoot(`RootTemplate.pptx`)
11+
.load(`SlideWithShapes.pptx`, 'shapes')
12+
.load(`SlideWithCharts.pptx`, 'charts')
13+
.load(`SlideWithImages.pptx`, 'images');
14+
15+
for (let i = 0; i <= 2; i++) {
16+
pres.addSlide('shapes', 1);
17+
}
18+
19+
pres.addSlide('charts', 1);
20+
pres.addSlide('charts', 2);
21+
pres.addSlide('images', 1);
22+
pres.addSlide('images', 2);
23+
24+
const order = [2, 1, 8, 7, 5, 6, 3, 4];
25+
pres.modify(ModifyPresentationHelper.sortSlides(order));
26+
27+
await pres.write(`sort-slides.test.pptx`);
28+
29+
expect(pres).toBeInstanceOf(Automizer);
30+
});

src/automizer.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { IPresentationProps } from './interfaces/ipresentation-props';
1010
import { PresTemplate } from './interfaces/pres-template';
1111
import { RootPresTemplate } from './interfaces/root-pres-template';
1212
import { Template } from './classes/template';
13-
import { TemplateInfo } from './types/xml-types';
13+
import { ModifyXmlCallback, TemplateInfo } from './types/xml-types';
1414
import { vd } from './helper/general-helper';
1515
import { Master } from './classes/master';
1616
import path from 'path';
1717
import * as fs from 'fs';
18+
import { XmlHelper } from './helper/xml-helper';
19+
import ModifyPresentationHelper from './helper/modify-presentation-helper';
1820

1921
/**
2022
* Automizer
@@ -40,12 +42,15 @@ export default class Automizer implements IPresentationProps {
4042
params: AutomizerParams;
4143
status: StatusTracker;
4244

45+
modifyPresentation: ModifyXmlCallback[];
46+
4347
/**
4448
* Creates an instance of `pptx-automizer`.
4549
* @param [params]
4650
*/
4751
constructor(params: AutomizerParams) {
4852
this.templates = [];
53+
this.modifyPresentation = [];
4954
this.params = params;
5055

5156
this.templateDir = params?.templateDir ? params.templateDir + '/' : '';
@@ -157,7 +162,7 @@ export default class Automizer implements IPresentationProps {
157162
/**
158163
* Parses all loaded templates and collects creationIds for slides and
159164
* elements. This will make finding templates and elements independent
160-
* from slide number and element name.
165+
* of slide number and element name.
161166
* @returns Promise<TemplateInfo[]>
162167
*/
163168
public async setCreationIds(): Promise<TemplateInfo[]> {
@@ -173,6 +178,11 @@ export default class Automizer implements IPresentationProps {
173178
return templateCreationId;
174179
}
175180

181+
public modify(cb: ModifyXmlCallback): this {
182+
this.modifyPresentation.push(cb);
183+
return this;
184+
}
185+
176186
/**
177187
* Determines whether template is root or default template.
178188
* @param template
@@ -262,8 +272,24 @@ export default class Automizer implements IPresentationProps {
262272
* @returns summary object.
263273
*/
264274
public async write(location: string): Promise<AutomizerSummary> {
275+
await this.writeSlides();
276+
await this.normalizePresentation();
277+
await this.applyModifyPresentationCallbacks();
278+
265279
const rootArchive = await this.rootTemplate.archive;
280+
const content = await rootArchive.generateAsync({ type: 'nodebuffer' });
281+
282+
return FileHelper.writeOutputFile(
283+
this.getLocation(location, 'output'),
284+
content,
285+
this,
286+
);
287+
}
266288

289+
/**
290+
* Write all slides into archive.
291+
*/
292+
public async writeSlides(): Promise<void> {
267293
await this.rootTemplate.countExistingSlides();
268294
this.status.max = this.rootTemplate.slides.length;
269295

@@ -274,16 +300,32 @@ export default class Automizer implements IPresentationProps {
274300
if (this.params.removeExistingSlides) {
275301
await this.rootTemplate.truncate();
276302
}
303+
}
277304

278-
const content = await rootArchive.generateAsync({ type: 'nodebuffer' });
279-
280-
return FileHelper.writeOutputFile(
281-
this.getLocation(location, 'output'),
282-
content,
283-
this,
305+
/**
306+
* Applies all callbacks in this.modifyPresentation-array.
307+
* The callback array can be pushed by this.modify()
308+
*/
309+
async applyModifyPresentationCallbacks(): Promise<void> {
310+
await XmlHelper.modifyXmlInArchive(
311+
this.rootTemplate.archive,
312+
`ppt/presentation.xml`,
313+
this.modifyPresentation,
284314
);
285315
}
286316

317+
/**
318+
* Apply some callbacks to restore archive/xml structure
319+
* and prevent corrupted pptx files.
320+
*
321+
* TODO: Remove unused parts (slides, related items) from archive.
322+
* TODO: Use every imported image only once
323+
* TODO: Check for lost relations
324+
*/
325+
normalizePresentation(): void {
326+
this.modify(ModifyPresentationHelper.normalizeSlideIds);
327+
}
328+
287329
/**
288330
* Applies path prefix to given location string.
289331
* @param location path and/or filename

src/classes/slide.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -631,13 +631,6 @@ export class Slide implements ISlide {
631631
parent: (xml: XMLDocument) => xml.getElementsByTagName('p:sldIdLst')[0],
632632
tag: 'p:sldId',
633633
attributes: {
634-
id: (xml: XMLDocument) =>
635-
XmlHelper.getMaxId(
636-
xml.getElementsByTagName('p:sldId'),
637-
'id',
638-
true,
639-
256,
640-
),
641634
'r:id': relId,
642635
} as SlideListAttribute,
643636
});

src/dev.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import Automizer, { ChartData, modify, TableRow, TableRowStyle } from './index';
1+
import Automizer, {
2+
ChartData,
3+
modify,
4+
TableRow,
5+
TableRowStyle,
6+
XmlHelper,
7+
} from './index';
28
import { vd } from './helper/general-helper';
9+
import ModifyPresentationHelper from './helper/modify-presentation-helper';
310

411
const automizer = new Automizer({
512
templateDir: `${__dirname}/../__tests__/pptx-templates`,
@@ -8,21 +15,19 @@ const automizer = new Automizer({
815
});
916

1017
const run = async () => {
11-
const pres = automizer
18+
const ppt = automizer
1219
.loadRoot(`RootTemplate.pptx`)
1320
.load(`SlideWithCharts.pptx`, 'charts')
1421
.load(`SlideWithImages.pptx`, 'images');
1522

16-
const result = await pres
17-
.addSlide('charts', 2, (slide) => {
18-
slide.removeElement('ColumnChart');
19-
})
20-
.addSlide('images', 2, (slide) => {
21-
slide.removeElement('imageJPG');
22-
slide.removeElement('Textfeld 5');
23-
slide.addElement('images', 2, 'imageJPG');
24-
})
25-
.write(`remove-element.test.pptx`);
23+
ppt.addSlide('charts', 1);
24+
ppt.addSlide('charts', 2);
25+
ppt.addSlide('images', 1);
26+
ppt.addSlide('images', 2);
27+
28+
ppt.modify(ModifyPresentationHelper.sortSlides([3, 2, 1]));
29+
30+
const summary = await ppt.write('reorder.pptx');
2631
};
2732

2833
run().catch((error) => {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { XmlHelper } from './xml-helper';
2+
import { vd } from './general-helper';
3+
4+
export default class ModifyPresentationHelper {
5+
/**
6+
* Get Collection of slides
7+
*/
8+
static getSlidesCollection = (xml: XMLDocument) => {
9+
return xml.getElementsByTagName('p:sldId');
10+
};
11+
12+
/**
13+
* Pass an array of slide numbers to define a target sort order.
14+
* First slide starts by 1.
15+
* @order Array of slide numbers, starting by 1
16+
*/
17+
static sortSlides = (order: number[]) => (xml: XMLDocument) => {
18+
const slides = ModifyPresentationHelper.getSlidesCollection(xml);
19+
order.map((index, i) => order[i]--);
20+
XmlHelper.sortCollection(slides, order);
21+
};
22+
23+
/**
24+
* Set ids to prevent corrupted pptx.
25+
* Must start with 256 and increment by one.
26+
*/
27+
static normalizeSlideIds = (xml: XMLDocument) => {
28+
const slides = ModifyPresentationHelper.getSlidesCollection(xml);
29+
const firstId = 256;
30+
XmlHelper.modifyCollection(slides, (slide: Element, i) => {
31+
slide.setAttribute('id', String(firstId + i));
32+
});
33+
};
34+
}

src/helper/xml-helper.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
OverrideAttribute,
88
RelationshipAttribute,
99
HelperElement,
10+
ModifyXmlCallback,
1011
} from '../types/xml-types';
1112
import { TargetByRelIdMap } from '../constants/constants';
1213
import { XmlPrettyPrint } from './xml-pretty-print';
@@ -19,6 +20,20 @@ import { XmlTemplateHelper } from './xml-template-helper';
1920
import { vd } from './general-helper';
2021

2122
export class XmlHelper {
23+
static async modifyXmlInArchive(
24+
archive: Promise<JSZip>,
25+
file: string,
26+
callbacks: ModifyXmlCallback[],
27+
): Promise<JSZip> {
28+
const xml = await XmlHelper.getXmlFromArchive(await archive, file);
29+
30+
for (const callback of callbacks) {
31+
callback(xml);
32+
}
33+
34+
return await XmlHelper.writeXmlToArchive(await archive, file, xml);
35+
}
36+
2237
static async getXmlFromArchive(
2338
archive: JSZip,
2439
file: string,
@@ -403,6 +418,39 @@ export class XmlHelper {
403418
}
404419
}
405420

421+
static sortCollection(
422+
collection: HTMLCollectionOf<Element>,
423+
order: number[],
424+
callback?: ModifyXmlCallback,
425+
): void {
426+
if (collection.length === 0) {
427+
return;
428+
}
429+
const parent = collection[0].parentNode;
430+
order.forEach((index, i) => {
431+
if (!collection[index]) {
432+
vd('sortCollection index not found' + index);
433+
return;
434+
}
435+
436+
const item = collection[index];
437+
if (callback) {
438+
callback(item, i);
439+
}
440+
parent.appendChild(item);
441+
});
442+
}
443+
444+
static modifyCollection(
445+
collection: HTMLCollectionOf<Element>,
446+
callback: ModifyXmlCallback,
447+
): void {
448+
for (let i = 0; i < collection.length; i++) {
449+
const item = collection[i];
450+
callback(item, i);
451+
}
452+
}
453+
406454
static dump(element: XMLDocument | Element): void {
407455
const s = new XMLSerializer();
408456
const xmlBuffer = s.serializeToString(element);

src/interfaces/itemplate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export interface ITemplate {
44
location: string;
55
file: InputType;
66
archive: Promise<JSZip>;
7+
getSlideIdList: () => Promise<Document>;
78
}

src/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export type AutomizerParams = {
2929
* You can set a path here.
3030
*/
3131
outputDir?: string;
32+
/**
33+
* Buffer unzipped pptx on disk
34+
*/
35+
cacheDir?: string;
3236
rootTemplate?: string;
3337
presTemplates?: string[];
3438
useCreationIds?: boolean;

src/types/xml-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ export type ElementInfo = {
6262
cy: number;
6363
};
6464
};
65+
66+
export type ModifyXmlCallback = (
67+
xml: XMLDocument | Element,
68+
index?: number,
69+
) => void;

0 commit comments

Comments
 (0)