diff --git a/es6/docUtils.js b/es6/docUtils.js index 246db4d..0fc5bf0 100644 --- a/es6/docUtils.js +++ b/es6/docUtils.js @@ -1,3 +1,6 @@ "use strict"; const DocUtils = require("docxtemplater").DocUtils; +DocUtils.convertPixelsToEmus = function (pixel) { + return Math.round(pixel * 9525); +}; module.exports = DocUtils; diff --git a/es6/imgManager.js b/es6/imgManager.js index dae66c0..50358ee 100644 --- a/es6/imgManager.js +++ b/es6/imgManager.js @@ -3,43 +3,47 @@ const DocUtils = require("./docUtils"); const extensionRegex = /[^.]+\.([^.]+)/; +const rels = { + getPrefix(fileType) { + return fileType === "docx" ? "word" : "ppt"; + }, + getFileTypeName(fileType) { + return fileType === "docx" ? "document" : "presentation"; + }, + getRelsFileName(fileName) { + return fileName.replace(/^.*?([a-zA-Z0-9]+)\.xml$/, "$1") + ".xml.rels"; + }, + getRelsFilePath(fileName, fileType) { + const relsFileName = rels.getRelsFileName(fileName); + const prefix = fileType === "pptx" ? "ppt/slides" : "word"; + return `${prefix}/_rels/${relsFileName}`; + }, +}; + module.exports = class ImgManager { - constructor(zip, fileName, xmlDocuments) { - this.fileType = this.getFileType(fileName); - this.fileTypeName = this.getFileTypeName(fileName); - this.relsFilePath = this.getRelsFile(fileName); + constructor(zip, fileName, xmlDocuments, fileType) { + this.fileName = fileName; + this.prefix = rels.getPrefix(fileType); this.zip = zip; this.xmlDocuments = xmlDocuments; - this.relsDoc = xmlDocuments[this.relsFilePath] || this.createEmptyRelsDoc(xmlDocuments, this.relsFilePath); - } - getRelsFile(fileName) { - let relsFilePath; - const relsFileName = this.getRelsFileName(fileName); - const fileType = this.getFileType(fileName); - if (fileType === "ppt") { - relsFilePath = "ppt/slides/_rels/" + relsFileName; - } - else { - relsFilePath = "word/_rels/" + relsFileName; - } - return relsFilePath; - } - getRelsFileName(fileName) { - return fileName.replace(/^.*?([a-z0-9]+)\.xml$/, "$1") + ".xml.rels"; - } - getFileType(fileName) { - return (fileName.indexOf("ppt/slides") === 0) ? "ppt" : "word"; - } - getFileTypeName(fileName) { - return (fileName.indexOf("ppt/slides") === 0) ? "presentation" : "document"; + this.fileTypeName = rels.getFileTypeName(fileType); + this.mediaPrefix = fileType === "pptx" ? "../media" : "media"; + const relsFilePath = rels.getRelsFilePath(fileName, fileType); + this.relsDoc = xmlDocuments[relsFilePath] || this.createEmptyRelsDoc(xmlDocuments, relsFilePath); } createEmptyRelsDoc(xmlDocuments, relsFileName) { - const file = this.zip.files[relsFileName] || this.zip.files[this.fileType + "/_rels/" + this.fileTypeName + ".xml.rels"]; - if (!file) { - return; + const mainRels = this.prefix + "/_rels/" + (this.fileTypeName) + ".xml.rels"; + const doc = xmlDocuments[mainRels]; + if (!doc) { + const err = new Error("Could not copy from empty relsdoc"); + err.properties = { + mainRels, + relsFileName, + files: Object.keys(this.zip.files), + }; + throw err; } - const content = DocUtils.decodeUtf8(file.asText()); - const relsDoc = DocUtils.str2xml(content); + const relsDoc = DocUtils.str2xml(DocUtils.xml2str(doc)); const relationships = relsDoc.getElementsByTagName("Relationships")[0]; const relationshipChilds = relationships.getElementsByTagName("Relationship"); for (let i = 0, l = relationshipChilds.length; i < l; i++) { @@ -81,11 +85,12 @@ module.exports = class ImgManager { i = 0; } const realImageName = i === 0 ? imageName : imageName + `(${i})`; - if (this.zip.files[`${this.fileType}/media/${realImageName}`] != null) { + const imagePath = `${this.prefix}/media/${realImageName}`; + if (this.zip.files[imagePath] != null) { return this.addImageRels(imageName, imageData, i + 1); } const image = { - name: `${this.fileType}/media/${realImageName}`, + name: imagePath, data: imageData, options: { binary: true, @@ -100,12 +105,7 @@ module.exports = class ImgManager { const maxRid = this.loadImageRels() + 1; newTag.setAttribute("Id", `rId${maxRid}`); newTag.setAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"); - if (this.fileType === "ppt") { - newTag.setAttribute("Target", `../media/${realImageName}`); - } - else { - newTag.setAttribute("Target", `media/${realImageName}`); - } + newTag.setAttribute("Target", `${this.mediaPrefix}/${realImageName}`); relationships.appendChild(newTag); return maxRid; } diff --git a/es6/index.js b/es6/index.js index 60ea7c4..d1e0ec6 100644 --- a/es6/index.js +++ b/es6/index.js @@ -1,5 +1,6 @@ "use strict"; +const templates = require("./templates"); const DocUtils = require("docxtemplater").DocUtils; const DOMParser = require("xmldom").DOMParser; @@ -10,23 +11,25 @@ function isNaN(number) { const ImgManager = require("./imgManager"); const moduleName = "open-xml-templating/docxtemplater-image-module"; -function getInner({part, left, right, postparsed}) { +function getInnerDocx({part}) { + return part; +} + +function getInnerPptx({part, left, right, postparsed}) { const xmlString = postparsed.slice(left + 1, right).reduce(function (concat, item) { return concat + item.value; }, ""); - part.off = {x: 0, y: 0}; - part.ext = {cx: 0, cy: 0}; const xmlDoc = new DOMParser().parseFromString("" + xmlString + ""); - const off = xmlDoc.getElementsByTagName("a:off"); - if (off.length > 0) { - part.off.x = off[0].getAttribute("x"); - part.off.y = off[0].getAttribute("y"); - } + const offset = xmlDoc.getElementsByTagName("a:off"); const ext = xmlDoc.getElementsByTagName("a:ext"); - if (ext.length > 0) { - part.ext.cx = ext[0].getAttribute("cx"); - part.ext.cy = ext[0].getAttribute("cy"); - } + part.ext = { + cx: parseInt(ext[0].getAttribute("cx"), 10), + cy: parseInt(ext[0].getAttribute("cy"), 10), + }; + part.offset = { + x: parseInt(offset[0].getAttribute("x"), 10), + y: parseInt(offset[0].getAttribute("y"), 10), + }; return part; } @@ -34,10 +37,10 @@ class ImageModule { constructor(options) { this.name = "ImageModule"; this.options = options || {}; + this.imgManagers = {}; if (this.options.centered == null) { this.options.centered = false; } if (this.options.getImage == null) { throw new Error("You should pass getImage"); } if (this.options.getSize == null) { throw new Error("You should pass getSize"); } - this.qrQueue = []; this.imageNumber = 1; } optionsTransformer(options, docxtemplater) { @@ -45,6 +48,7 @@ class ImageModule { .concat(docxtemplater.zip.file(/\[Content_Types\].xml/)) .map((file) => file.name); this.fileTypeConfig = docxtemplater.fileTypeConfig; + this.fileType = docxtemplater.fileType; this.zip = docxtemplater.zip; options.xmlFileNames = options.xmlFileNames.concat(relsFiles); return options; @@ -70,16 +74,20 @@ class ImageModule { } postparse(parsed) { let expandTo; + let getInner; if (this.options.fileType === "pptx") { expandTo = "p:sp"; + getInner = getInnerPptx; } else { expandTo = this.options.centered ? "w:p" : "w:t"; + getInner = getInnerDocx; } return DocUtils.traits.expandToOne(parsed, {moduleName, getInner, expandTo}); } render(part, options) { - this.imgManager = new ImgManager(this.zip, options.filePath, this.xmlDocuments); + this.imgManagers[options.filePath] = this.imgManagers[options.filePath] || new ImgManager(this.zip, options.filePath, this.xmlDocuments, this.fileType); + const imgManager = this.imgManagers[options.filePath]; if (!part.type === "placeholder" || part.module !== moduleName) { return null; } @@ -89,7 +97,7 @@ class ImageModule { throw new Error("tagValue is empty"); } const imgBuffer = this.options.getImage(tagValue, part.value); - const rId = this.imgManager.addImageRels(this.getNextImageName(), imgBuffer); + const rId = imgManager.addImageRels(this.getNextImageName(), imgBuffer); const sizePixel = this.options.getSize(imgBuffer, tagValue, part.value); return this.getRenderedPart(part, rId, sizePixel); } @@ -98,220 +106,42 @@ class ImageModule { } } getRenderedPart(part, rId, sizePixel) { - const size = [this.convertPixelsToEmus(sizePixel[0]), this.convertPixelsToEmus(sizePixel[1])]; + if (isNaN(rId)) { + throw new Error("rId is NaN, aborting"); + } + const size = [DocUtils.convertPixelsToEmus(sizePixel[0]), DocUtils.convertPixelsToEmus(sizePixel[1])]; const centered = (this.options.centered || part.centered); let newText; if (this.options.fileType === "pptx") { - newText = this.getPptRender(part, rId, size, centered); + newText = this.getRenderedPartPptx(part, rId, size, centered); } else { - newText = this.getDocxRender(part, rId, size, centered); + newText = this.getRenderedPartDocx(rId, size, centered); } return {value: newText}; } - getPptRender(part, rId, size, centered) { - const offset = {x: parseInt(part.off.x, 10), y: parseInt(part.off.y, 10)}; - const cellCX = parseInt(part.ext.cx, 10) || 1; - const cellCY = parseInt(part.ext.cy, 10) || 1; - const imgW = parseInt(size[0], 10) || 1; - const imgH = parseInt(size[1], 10) || 1; + getRenderedPartPptx(part, rId, size, centered) { + const offset = {x: part.offset.x, y: part.offset.y}; + const cellCX = part.ext.cx; + const cellCY = part.ext.cy; + const imgW = size[0]; + const imgH = size[1]; if (centered) { - offset.x = offset.x + (cellCX / 2) - (imgW / 2); - offset.y = offset.y + (cellCY / 2) - (imgH / 2); + offset.x += cellCX / 2 - imgW / 2; + offset.y += cellCY / 2 - imgH / 2; } - return this.getPptImageXml(rId, [imgW, imgH], offset); + return templates.getPptxImageXml(rId, [imgW, imgH], offset); } - getDocxRender(part, rId, size, centered) { - return (centered) ? this.getImageXmlCentered(rId, size) : this.getImageXml(rId, size); + getRenderedPartDocx(rId, size, centered) { + return centered ? templates.getImageXmlCentered(rId, size) : templates.getImageXml(rId, size); } getNextImageName() { const name = `image_generated_${this.imageNumber}.png`; this.imageNumber++; return name; } - convertPixelsToEmus(pixel) { - return Math.round(pixel * 9525); - } - getImageXml(rId, size) { - if (isNaN(rId)) { - throw new Error("rId is NaN, aborting"); - } - return ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `.replace(/\t|\n/g, ""); - } - getImageXmlCentered(rId, size) { - if (isNaN(rId)) { - throw new Error("rId is NaN, aborting"); - } - return ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `.replace(/\t|\n/g, ""); - } - getPptImageXml(rId, size, off) { - if (isNaN(rId)) { - throw new Error("rId is NaN, aborting"); - } - return ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `.replace(/\t|\n/g, ""); - } } module.exports = ImageModule; diff --git a/es6/templates.js b/es6/templates.js new file mode 100644 index 0000000..d7b0fc8 --- /dev/null +++ b/es6/templates.js @@ -0,0 +1,172 @@ +module.exports = { + getImageXml(rId, size) { + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `.replace(/\t|\n/g, ""); + }, + getImageXmlCentered(rId, size) { + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `.replace(/\t|\n/g, ""); + }, + getPptxImageXml(rId, size, offset) { + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `.replace(/\t|\n/g, ""); + }, +}; + diff --git a/es6/test.js b/es6/test.js index 43e3868..a91a3ac 100644 --- a/es6/test.js +++ b/es6/test.js @@ -22,6 +22,8 @@ const fileNames = [ "expectedWithoutRels.docx", "tagImage.pptx", "expectedTagImage.pptx", + "tagImageCentered.pptx", + "expectedTagImageCentered.pptx", ]; beforeEach(function () { @@ -104,6 +106,13 @@ function testStart() { this.data = {image: "examples/image.png"}; this.loadAndRender(); }); + + it("should work with PPTX documents centered", function () { + this.name = "tagImageCentered.pptx"; + this.expectedName = "expectedTagImageCentered.pptx"; + this.data = {image: "examples/image.png"}; + this.loadAndRender(); + }); }); } diff --git a/examples/expectedTagImage.pptx b/examples/expectedTagImage.pptx index 9e0bb99..e6277f1 100644 Binary files a/examples/expectedTagImage.pptx and b/examples/expectedTagImage.pptx differ diff --git a/examples/expectedTagImageCentered.pptx b/examples/expectedTagImageCentered.pptx new file mode 100644 index 0000000..c7d5cee Binary files /dev/null and b/examples/expectedTagImageCentered.pptx differ diff --git a/examples/tagImageCentered.pptx b/examples/tagImageCentered.pptx new file mode 100644 index 0000000..eb8ad27 Binary files /dev/null and b/examples/tagImageCentered.pptx differ diff --git a/package.json b/package.json index 2eaf1b3..d4b01a2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Image Module for docxtemplater", "main": "js/index.js", "scripts": { + "test:coverage": "istanbul cover _mocha -- es6/test.js", "compile": "rimraf js && mkdirp js && babel es6 --out-dir js", "preversion": "npm test && npm run compile && npm run browserify && npm run uglify", "test:compiled": "mocha js/test.js", @@ -24,6 +25,7 @@ "jszip": "^2.6.1", "mkdirp": "^0.5.1", "mocha": "^3.2.0", + "istanbul": "^0.4.5", "rimraf": "^2.5.4", "uglifyjs": "^2.4.10" },