UNPKG

er-nodejs-pptx

Version:

Generate PPTX files on the server-side with JavaScript.

273 lines (218 loc) 10 kB
/* Factories take a JSON payload and returns a hydrated fragment with the attributes of the JSON applied within. */ /* eslint-disable no-prototype-builtins */ const JSZip = require('jszip'); const xml2js = require('xml2js'); const request = require('request').defaults({ encoding: null }); let { PptxContentHelper } = require('../helpers/pptx-content-helper'); let { ContentTypeFactory } = require('./content-types'); let { DocPropsFactory } = require('./doc-props'); let { PptFactory } = require('./ppt'); let { RelsFactory } = require('./rels'); let { Slide } = require('../slide'); class PowerPointFactory { constructor(presentation, args) { this.content = presentation.content; this.presentation = presentation; this.args = args; this.slides = {}; this.charts = {}; this.contentTypeFactory = new ContentTypeFactory(this, args); this.docPropsFactory = new DocPropsFactory(this, args); this.relsFactory = new RelsFactory(this, args); this.pptFactory = new PptFactory(this, args); this.build(); // this will build the _initial_ content from our fragments this.extractObjectsFromContent(this.content); } async loadFromRawFileData(data) { this.clearContent(); let self = this; let zip = new JSZip(); await zip.loadAsync(data); for (let key in zip.files) { if (Object.prototype.hasOwnProperty.call(zip.files, key)) { let ext = key.substr(key.lastIndexOf('.')); if (ext === '.xml' || ext === '.rels') { let js = await zip.file(key).async('string'); xml2js.parseString(js, function(err, js) { self.content[key] = js; }); } else { // skip dir names if (key[key.length - 1] !== '/') { this.content[key] = await zip.file(key).async('nodebuffer'); } } } } this.extractObjectsFromContent(this.content); } clearContent() { for (let key in this.content) { if (this.content.hasOwnProperty(key)) { delete this.content[key]; } } } build() { // Build the default document structure needed by a presentation. // The user will have the ability to override any of these details but // this will provide a collection of sensible defaults. this.contentTypeFactory.build(); this.docPropsFactory.build(); this.relsFactory.build(); this.pptFactory.build(); } extractObjectsFromContent(content) { let slideInformation = PptxContentHelper.extractInfoFromSlides(content); for (let slideName in slideInformation) { if (slideInformation.hasOwnProperty(slideName)) { this.slides[slideName] = new Slide({ parentContainer: this.presentation, powerPointFactory: this, content: content[`ppt/slides/${slideName}.xml`], name: slideName, layoutName: slideInformation[slideName].layout.name, externalObjectCount: slideInformation[slideName].objectCount, fromExternalSource: true, }); } } // TODO: Now we need to extract chart info if an existing pptx is being loaded // _and_ that pptx contains charts. Won't affect anything if there are no charts // in the pptx. } setPowerPointProperties(props) { this.docPropsFactory.setProperties(props); } getPowerPointProperties() { return this.docPropsFactory.getProperties(); } setLayout(layout) { this.pptFactory.setLayout(layout); } setBackgroundColor(slide, color) { this.pptFactory.setBackgroundColor(slide, color); } getSlide(slideName) { if (!this.slides.hasOwnProperty(slideName)) throw new Error(`Slide name doesn't exist in PowerPointFactory.getSlide(): '${slideName}' `); return this.slides[slideName]; } addSlide(layoutName) { let slideName = `slide${Object.keys(this.slides).length + 1}`; let newSlideContentBlock = this.pptFactory.addSlide(slideName, layoutName); let slide = new Slide({ powerPointFactory: this, content: newSlideContentBlock, name: slideName, layoutName: layoutName, }); this.contentTypeFactory.addContentType( `/ppt/slides/${slideName}.xml`, 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml' ); this.slides[slideName] = slide; this.docPropsFactory.incrementSlideCount(); return slide; } removeSlide(slideName) { this.pptFactory.removeSlide(slideName); this.contentTypeFactory.removeContentType(`/ppt/slides/${slideName}.xml`); this.docPropsFactory.decrementSlideCount(); delete this.slides[slideName]; } moveSlide(sourceSlideNum, destinationSlideNum) { let sourceSlideName = `slide${sourceSlideNum}`; let destinationSlideName = `slide${destinationSlideNum}`; if (!this.slides.hasOwnProperty(sourceSlideName)) { throw new Error(`Source slide number does not exist in PowerPointFactory.moveSlide(): ${sourceSlideNum}`); } if (!this.slides.hasOwnProperty(destinationSlideName)) { throw new Error(`Destination slide number does not exist in PowerPointFactory.moveSlide(): ${destinationSlideNum}`); } this.pptFactory.moveSlide(sourceSlideNum, destinationSlideNum); let self = this; let internalSwap = function(index) { try { let slideName1 = `slide${index}`; let slideName2 = `slide${index + 1}`; [self.slides[slideName1], self.slides[slideName2]] = [self.slides[slideName2], self.slides[slideName1]]; // rename internal slide name identifiers // (don't confuse slideName1(2) to slideName1(2) - since the line above already swapped the name props, slide1 is really becoming slide2 and vice-versa) self.slides[slideName1].rename(slideName1); self.slides[slideName2].rename(slideName2); } catch (err) { console.warn(err); throw err; } }; if (destinationSlideNum > sourceSlideNum) { // move slides between start and destination backwards (e.g. slide 4 becomes 3, 3 becomes 2, etc.) for (let i = sourceSlideNum; i < destinationSlideNum; i++) { internalSwap(i); } } else if (destinationSlideNum < sourceSlideNum) { // move slides between start and destination forward (e.g. slide 2 becomes 3, 3 becomes 4, etc.) for (let i = sourceSlideNum - 1; i >= destinationSlideNum; i--) { internalSwap(i); } } } addImage(slide, image) { image.setContent(this.pptFactory.addImage(slide, image)); } async addImageFromRemoteUrl(slide, image) { image.source = await new Promise(function(resolve, reject) { request.get(image.downloadUrl, { timeout: 30000 }, function(err, res, buffer) { if (err) reject(err); resolve(buffer); }); }); return this.addImage(slide, image); } addText(slide, textBox) { textBox.setContent(this.pptFactory.addText(slide, textBox)); } addShape(slide, shape) { shape.setContent(this.pptFactory.addShape(slide, shape)); } async addChart(slide, chart) { chart.name = `chart${Object.keys(this.charts).length + 1}`; chart.setContent(await this.pptFactory.addChart(slide, chart)); this.contentTypeFactory.addContentType(`/ppt/charts/${chart.name}.xml`, 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'); this.contentTypeFactory.addDefaultContentType(`xlsx`, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); this.charts[chart.name] = chart; } getWorksheetCount() { // TODO... (implement this in higher level factory) return Object.keys(this.content).filter(function(key) { return key.substr(0, 36) === 'ppt/embeddings/Microsoft_Excel_Sheet'; }).length; } addDefaultMediaContentTypes() { this.contentTypeFactory.addDefaultMediaContentTypes(); } // NOTE: this function is for future use... but it works! (you would call it before writing the buffer in presentation.js) rebuild() { this.content = {}; this.build(); // build the base from our fragments // add anything new the user has added for (let slideName in this.slides) { if (this.slides.hasOwnProperty(slideName)) { let slide = this.slides[slideName]; if (!slide.fromExternalSource) { this.pptFactory.addSlide(slideName, slide.layoutName); this.contentTypeFactory.addContentType( `/ppt/slides/${slideName}.xml`, 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml' ); } } } // add other objects here... this.docPropsFactory.setSlideCount(Object.keys(this.slides).length); } } module.exports.PowerPointFactory = PowerPointFactory;