Commit 1e0b44a1 by Maxime Richard Committed by Edgar HIPP

Support for PPTX documents

Images using {%image}
Centered images using {%%image} (per image setting ) or opts.centered = true (global)
Centering tag works with DOCX documents too
parent b165e7fd
/*.docx /*.docx
/*.pptx
test/ test/
js/ js/
node_modules node_modules
coverage/ coverage/
build build
.idea/
...@@ -5,19 +5,38 @@ const extensionRegex = /[^.]+\.([^.]+)/; ...@@ -5,19 +5,38 @@ const extensionRegex = /[^.]+\.([^.]+)/;
module.exports = class ImgManager { module.exports = class ImgManager {
constructor(zip, fileName, xmlDocuments) { constructor(zip, fileName, xmlDocuments) {
this.fileType = this.getFileType(fileName);
this.fileTypeName = this.getFileTypeName(fileName);
this.relsFilePath = this.getRelsFile(fileName);
this.zip = zip; this.zip = zip;
this.xmlDocuments = xmlDocuments; this.xmlDocuments = xmlDocuments;
const relsFileName = this.getRelsFile(fileName); this.relsDoc = xmlDocuments[this.relsFilePath] || this.createEmptyRelsDoc(xmlDocuments, this.relsFilePath);
this.relsDoc = xmlDocuments[relsFileName] || this.createEmptyRelsDoc(xmlDocuments, relsFileName);
} }
getRelsFile(fileName) { getRelsFile(fileName) {
let fileParts = fileName.split("/"); let relsFilePath;
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1] + ".rels"; let relsFileName = this.getRelsFileName(fileName);
fileParts = [fileParts[0], "_rels"].concat(fileParts.slice(1)); let fileType = this.getFileType(fileName);
return fileParts.join("/"); 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";
} }
createEmptyRelsDoc(xmlDocuments, relsFileName) { createEmptyRelsDoc(xmlDocuments, relsFileName) {
const file = this.zip.files["word/_rels/document.xml.rels"]; const file = this.zip.files[relsFileName] || this.zip.files[this.fileType + "/_rels/" + this.fileTypeName + ".xml.rels"];
if (!file) {
return;
}
const content = DocUtils.decodeUtf8(file.asText()); const content = DocUtils.decodeUtf8(file.asText());
const relsDoc = DocUtils.str2xml(content); const relsDoc = DocUtils.str2xml(content);
const relationships = relsDoc.getElementsByTagName("Relationships")[0]; const relationships = relsDoc.getElementsByTagName("Relationships")[0];
...@@ -61,11 +80,11 @@ module.exports = class ImgManager { ...@@ -61,11 +80,11 @@ module.exports = class ImgManager {
i = 0; i = 0;
} }
const realImageName = i === 0 ? imageName : imageName + `(${i})`; const realImageName = i === 0 ? imageName : imageName + `(${i})`;
if (this.zip.files[`word/media/${realImageName}`] != null) { if (this.zip.files[`${this.fileType}/media/${realImageName}`] != null) {
return this.addImageRels(imageName, imageData, i + 1); return this.addImageRels(imageName, imageData, i + 1);
} }
const image = { const image = {
name: `word/media/${realImageName}`, name: `${this.fileType}/media/${realImageName}`,
data: imageData, data: imageData,
options: { options: {
binary: true, binary: true,
...@@ -80,7 +99,11 @@ module.exports = class ImgManager { ...@@ -80,7 +99,11 @@ module.exports = class ImgManager {
const maxRid = this.loadImageRels() + 1; const maxRid = this.loadImageRels() + 1;
newTag.setAttribute("Id", `rId${maxRid}`); newTag.setAttribute("Id", `rId${maxRid}`);
newTag.setAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"); 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", `media/${realImageName}`);
}
relationships.appendChild(newTag); relationships.appendChild(newTag);
return maxRid; return maxRid;
} }
......
"use strict"; "use strict";
const DocUtils = require("docxtemplater").DocUtils; const DocUtils = require("docxtemplater").DocUtils;
const DOMParser = require("xmldom").DOMParser;
function isNaN(number) { function isNaN(number) {
return !(number === number); return !(number === number);
} }
...@@ -8,7 +10,29 @@ function isNaN(number) { ...@@ -8,7 +10,29 @@ function isNaN(number) {
const ImgManager = require("./imgManager"); const ImgManager = require("./imgManager");
const moduleName = "open-xml-templating/docxtemplater-image-module"; const moduleName = "open-xml-templating/docxtemplater-image-module";
function getInner({part}) { function getInner({part, left, right, postparsed, index}) {
let xmlString = postparsed.slice(left + 1, right).reduce(function (concat, item) {
return concat + item.value;
}, "");
part.off = {};
part.ext = {};
var xmlDoc = new DOMParser().parseFromString("<xml>" + xmlString + "</xml>");
var off = xmlDoc.getElementsByTagName("a:off");
if (off.length > 0) {
part.off.x = off[0].getAttribute("x");
part.off.y = off[0].getAttribute("y");
}
var ext = xmlDoc.getElementsByTagName("a:ext");
if (ext.length > 0) {
part.ext.cx = ext[0].getAttribute("cx");
part.ext.cy = ext[0].getAttribute("cy");
}
if (part.off.x == null || part.off.y == null || part.ext.cx == null || part.ext.cy == null) {
part.off.x = 0;
part.off.y = 0;
part.ext.cx = 0;
part.ext.cy = 0;
}
return part; return part;
} }
...@@ -42,13 +66,21 @@ class ImageModule { ...@@ -42,13 +66,21 @@ class ImageModule {
parse(placeHolderContent) { parse(placeHolderContent) {
const module = moduleName; const module = moduleName;
const type = "placeholder"; const type = "placeholder";
if (placeHolderContent[0] === "%") { if (placeHolderContent.substring(0, 2) === "%%") {
return {type, value: placeHolderContent.substr(1), module}; return {type, value: placeHolderContent.substr(2), module, centered: true};
}
if (placeHolderContent.substring(0, 1) === "%") {
return {type, value: placeHolderContent.substr(1), module, centered: false};
} }
return null; return null;
} }
postparse(parsed) { postparse(parsed) {
const expandTo = this.options.centered ? "w:p" : "w:t"; let expandTo;
if (this.options.fileType == "pptx") {
expandTo = "p:sp";
} else {
expandTo = this.options.centered ? "w:p" : "w:t";
}
return DocUtils.traits.expandToOne(parsed, {moduleName, getInner, expandTo}); return DocUtils.traits.expandToOne(parsed, {moduleName, getInner, expandTo});
} }
render(part, options) { render(part, options) {
...@@ -74,7 +106,24 @@ class ImageModule { ...@@ -74,7 +106,24 @@ class ImageModule {
const rId = this.imgManager.addImageRels(this.getNextImageName(), imgBuffer); const rId = this.imgManager.addImageRels(this.getNextImageName(), imgBuffer);
const sizePixel = this.options.getSize(imgBuffer, tagValue, part.value); const sizePixel = this.options.getSize(imgBuffer, tagValue, part.value);
const size = [this.convertPixelsToEmus(sizePixel[0]), this.convertPixelsToEmus(sizePixel[1])]; const size = [this.convertPixelsToEmus(sizePixel[0]), this.convertPixelsToEmus(sizePixel[1])];
const newText = this.options.centered ? this.getImageXmlCentered(rId, size) : this.getImageXml(rId, size); let newText;
if (this.options.fileType == "pptx") {
let offset = {x: parseInt(part.off.x, 10), y: parseInt(part.off.y, 10)};
let cellCX = parseInt(part.ext.cx, 10) || 1;
let cellCY = parseInt(part.ext.cy, 10) || 1;
let imgW = parseInt(size[0], 10) || 1;
let imgH = parseInt(size[1], 10) || 1;
if (this.options.centered || part.centered) {
offset.x = offset.x + (cellCX / 2) - (imgW / 2);
offset.y = offset.y + (cellCY / 2) - (imgH / 2);
}
newText = this.getPPTImageXml(rId, [imgW, imgH], offset);
} else {
newText = (this.options.centered || part.centered) ? this.getImageXmlCentered(rId, size) : this.getImageXml(rId, size);
}
return {value: newText}; return {value: newText};
} }
getNextImageName() { getNextImageName() {
...@@ -193,6 +242,76 @@ class ImageModule { ...@@ -193,6 +242,76 @@ class ImageModule {
</w:p> </w:p>
`.replace(/\t|\n/g, ""); `.replace(/\t|\n/g, "");
} }
getPPTImageXml(rId, size, off) {
if (isNaN(rId)) {
throw new Error("rId is NaN, aborting");
}
return `<p:pic>
<p:nvPicPr>
<p:cNvPr id="6" name="Picture 2"/>
<p:cNvPicPr>
<a:picLocks noChangeAspect="1" noChangeArrowheads="1"/>
</p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rId${rId}" cstate="print">
<a:extLst>
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
</a:ext>
</a:extLst>
</a:blip>
<a:srcRect/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</p:blipFill>
<p:spPr bwMode="auto">
<a:xfrm>
<a:off x="${off.x}" y="${off.y}"/>
<a:ext cx="${size[0]}" cy="${size[1]}"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
<a:noFill/>
<a:ln>
<a:noFill/>
</a:ln>
<a:effectLst/>
<a:extLst>
<a:ext uri="{909E8E84-426E-40DD-AFC4-6F175D3DCCD1}">
<a14:hiddenFill xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main">
<a:solidFill>
<a:schemeClr val="accent1"/>
</a:solidFill>
</a14:hiddenFill>
</a:ext>
<a:ext uri="{91240B29-F687-4F45-9708-019B960494DF}">
<a14:hiddenLine xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" w="9525">
<a:solidFill>
<a:schemeClr val="tx1"/>
</a:solidFill>
<a:miter lim="800000"/>
<a:headEnd/>
<a:tailEnd/>
</a14:hiddenLine>
</a:ext>
<a:ext uri="{AF507438-7753-43E0-B8FC-AC1667EBCBE1}">
<a14:hiddenEffects xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main">
<a:effectLst>
<a:outerShdw dist="35921" dir="2700000" algn="ctr" rotWithShape="0">
<a:schemeClr val="bg2"/>
</a:outerShdw>
</a:effectLst>
</a14:hiddenEffects>
</a:ext>
</a:extLst>
</p:spPr>
</p:pic>
`.replace(/\t|\n/g, "");
}
} }
module.exports = ImageModule; module.exports = ImageModule;
...@@ -20,6 +20,8 @@ const fileNames = [ ...@@ -20,6 +20,8 @@ const fileNames = [
"expectedLoopCentered.docx", "expectedLoopCentered.docx",
"withoutRels.docx", "withoutRels.docx",
"expectedWithoutRels.docx", "expectedWithoutRels.docx",
"tagImage.pptx",
"expectedTagImage.pptx"
]; ];
beforeEach(function () { beforeEach(function () {
...@@ -30,12 +32,16 @@ beforeEach(function () { ...@@ -30,12 +32,16 @@ beforeEach(function () {
getSize: function () { getSize: function () {
return [150, 150]; return [150, 150];
}, },
centered: false, centered: false
}; };
this.loadAndRender = function () { this.loadAndRender = function () {
var fileType = (testutils.pptX[this.name]) ? 'pptx' : 'docx';
var file = (fileType == 'pptx') ? testutils.pptX[this.name] : testutils.docX[this.name];
this.doc = new Docxtemplater(); this.doc = new Docxtemplater();
const inputZip = new JSZip(testutils.docX[this.name].loadedContent); this.doc.setOptions({fileType});
this.opts.fileType = fileType;
const inputZip = new JSZip(file.loadedContent);
this.doc.loadZip(inputZip).setData(this.data); this.doc.loadZip(inputZip).setData(this.data);
const imageModule = new ImageModule(this.opts); const imageModule = new ImageModule(this.opts);
this.doc.attachModule(imageModule); this.doc.attachModule(imageModule);
...@@ -51,6 +57,7 @@ function testStart() { ...@@ -51,6 +57,7 @@ function testStart() {
this.name = "imageExample.docx"; this.name = "imageExample.docx";
this.expectedName = "expectedOneImage.docx"; this.expectedName = "expectedOneImage.docx";
this.data = {image: "examples/image.png"}; this.data = {image: "examples/image.png"};
this.fileType = 'docx';
this.loadAndRender(); this.loadAndRender();
}); });
...@@ -90,12 +97,23 @@ function testStart() { ...@@ -90,12 +97,23 @@ function testStart() {
this.data = {image: "examples/image.png"}; this.data = {image: "examples/image.png"};
this.loadAndRender(); this.loadAndRender();
}); });
it("should work with PPTX documents", function () {
this.name = "tagImage.pptx";
this.expectedName = "expectedTagImage.pptx";
this.data = {image: "examples/image.png"};
this.loadAndRender();
});
}); });
} }
testutils.setExamplesDirectory(path.resolve(__dirname, "..", "examples")); testutils.setExamplesDirectory(path.resolve(__dirname, "..", "examples"));
testutils.setStartFunction(testStart); testutils.setStartFunction(testStart);
fileNames.forEach(function (filename) { fileNames.forEach(function (filename) {
if (filename.endsWith('pptx')) {
testutils.loadFile(filename, testutils.loadPptx);
} else {
testutils.loadFile(filename, testutils.loadDocx); testutils.loadFile(filename, testutils.loadDocx);
}
}); });
testutils.start(); testutils.start();
...@@ -33,5 +33,7 @@ ...@@ -33,5 +33,7 @@
}, },
"author": "Edgar Hipp", "author": "Edgar Hipp",
"license": "MIT", "license": "MIT",
"dependencies": {} "dependencies": {
"xmldom": "^0.1.27"
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment