@jsreport/jsreport-pptx
Advanced tools
Comparing version 3.2.1 to 3.3.0
const sizeOf = require('image-size') | ||
const { nodeListToArray } = require('../utils') | ||
const axios = require('axios') | ||
const { nodeListToArray, pxToEMU, cmToEMU } = require('../utils') | ||
module.exports = (files) => { | ||
module.exports = async (files) => { | ||
const contentTypesFile = files.find(f => f.path === '[Content_Types].xml') | ||
@@ -30,6 +31,40 @@ const types = contentTypesFile.doc.getElementsByTagName('Types')[0] | ||
const el = imagesEl[i] | ||
const imageSrc = el.getAttribute('src') | ||
const imageExtensions = imageSrc.split(';')[0].split('/')[1] | ||
const imageBuffer = Buffer.from(imageSrc.split(';')[1].substring('base64,'.length), 'base64') | ||
if (!el.textContent.includes('$pptxImage')) { | ||
throw new Error('Invalid pptxImage element') | ||
} | ||
const match = el.textContent.match(/\$pptxImage([^$]*)\$/) | ||
const imageConfig = JSON.parse(Buffer.from(match[1], 'base64').toString()) | ||
let imageExtension | ||
let imageBuffer | ||
if (imageConfig.src && imageConfig.src.startsWith('data:')) { | ||
imageExtension = imageConfig.src.split(';')[0].split('/')[1] | ||
imageBuffer = Buffer.from(imageConfig.src.split(';')[1].substring('base64,'.length), 'base64') | ||
} else { | ||
const response = await axios({ | ||
url: imageConfig.src, | ||
responseType: 'arraybuffer', | ||
method: 'GET' | ||
}) | ||
const contentType = response.headers['content-type'] || response.headers['Content-Type'] | ||
if (!contentType) { | ||
throw new Error(`Empty content-type for remote image at "${imageConfig.src}"`) | ||
} | ||
const extensionsParts = contentType.split(';')[0].split('/').filter((p) => p) | ||
if (extensionsParts.length === 0 || extensionsParts.length > 2) { | ||
throw new Error(`Invalid content-type "${contentType}" for remote image at "${imageConfig.src}"`) | ||
} | ||
// some servers returns the image content type without the "image/" prefix | ||
imageExtension = extensionsParts.length === 1 ? extensionsParts[0] : extensionsParts[1] | ||
imageBuffer = Buffer.from(response.data) | ||
} | ||
imagesStartId++ | ||
@@ -39,6 +74,6 @@ const relEl = relsDoc.createElement('Relationship') | ||
relEl.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') | ||
relEl.setAttribute('Target', `../media/imageJsReport${imagesStartId}.${imageExtensions}`) | ||
relEl.setAttribute('Target', `../media/imageJsReport${imagesStartId}.${imageExtension}`) | ||
files.push({ | ||
path: `ppt/media/imageJsReport${imagesStartId}.${imageExtensions}`, | ||
path: `ppt/media/imageJsReport${imagesStartId}.${imageExtension}`, | ||
data: imageBuffer | ||
@@ -49,11 +84,64 @@ }) | ||
const existsTypeForImageExtension = nodeListToArray(types.getElementsByTagName('Default')).find( | ||
d => d.getAttribute('Extension') === imageExtension | ||
) != null | ||
if (!existsTypeForImageExtension) { | ||
const newDefault = contentTypesFile.doc.createElement('Default') | ||
newDefault.setAttribute('Extension', imageExtension) | ||
newDefault.setAttribute('ContentType', `image/${imageExtension}`) | ||
types.appendChild(newDefault) | ||
} | ||
const grpSp = el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode | ||
const aExt = grpSp.getElementsByTagName('p:grpSpPr')[0].getElementsByTagName('a:xfrm')[0].getElementsByTagName('a:ext')[0] | ||
const blip = grpSp.getElementsByTagName('a:blip')[0] | ||
let imageWidthEMU | ||
let imageHeightEMU | ||
if (imageConfig.width != null || imageConfig.height != null) { | ||
const imageDimension = sizeOf(imageBuffer) | ||
const targetWidth = getDimension(imageConfig.width) | ||
const targetHeight = getDimension(imageConfig.height) | ||
if (targetWidth) { | ||
imageWidthEMU = | ||
targetWidth.unit === 'cm' | ||
? cmToEMU(targetWidth.value) | ||
: pxToEMU(targetWidth.value) | ||
} | ||
if (targetHeight) { | ||
imageHeightEMU = | ||
targetHeight.unit === 'cm' | ||
? cmToEMU(targetHeight.value) | ||
: pxToEMU(targetHeight.value) | ||
} | ||
if (imageWidthEMU != null && imageHeightEMU == null) { | ||
// adjust height based on aspect ratio of image | ||
imageHeightEMU = Math.round( | ||
imageWidthEMU * | ||
(pxToEMU(imageDimension.height) / pxToEMU(imageDimension.width)) | ||
) | ||
} else if (imageHeightEMU != null && imageWidthEMU == null) { | ||
// adjust width based on aspect ratio of image | ||
imageWidthEMU = Math.round( | ||
imageHeightEMU * | ||
(pxToEMU(imageDimension.width) / pxToEMU(imageDimension.height)) | ||
) | ||
} | ||
} else if (imageConfig.usePlaceholderSize) { | ||
// taking existing size defined in word | ||
imageWidthEMU = parseFloat(aExt.getAttribute('cx')) | ||
imageHeightEMU = parseFloat(aExt.getAttribute('cy')) | ||
} else { | ||
const imageDimension = sizeOf(imageBuffer) | ||
imageWidthEMU = pxToEMU(imageDimension.width) | ||
imageHeightEMU = pxToEMU(imageDimension.height) | ||
} | ||
blip.setAttribute('r:embed', `rId${imagesStartId}`) | ||
const imageDimension = sizeOf(imageBuffer) | ||
const imageWidthEMU = Math.round(imageDimension.width * 914400 / 96) | ||
const imageHeightEMU = Math.round(imageDimension.height * 914400 / 96) | ||
const aExt = grpSp.getElementsByTagName('a:ext')[0] | ||
aExt.setAttribute('cx', imageWidthEMU) | ||
@@ -66,1 +154,15 @@ aExt.setAttribute('cy', imageHeightEMU) | ||
} | ||
function getDimension (value) { | ||
const regexp = /^(\d+(.\d+)?)(cm|px)$/ | ||
const match = regexp.exec(value) | ||
if (match) { | ||
return { | ||
value: parseFloat(match[1]), | ||
unit: match[3] | ||
} | ||
} | ||
return null | ||
} |
const slides = require('./slides') | ||
const image = require('./image') | ||
module.exports = (files) => { | ||
module.exports = async (files) => { | ||
slides(files) | ||
image(files) | ||
await image(files) | ||
} |
@@ -10,65 +10,80 @@ const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') | ||
const files = await decompress()(pptxTemplateContent) | ||
try { | ||
let files | ||
for (const f of files) { | ||
if (contentIsXML(f.data)) { | ||
f.doc = new DOMParser().parseFromString(f.data.toString()) | ||
f.data = f.data.toString() | ||
try { | ||
files = await decompress()(pptxTemplateContent) | ||
} catch (parseTemplateError) { | ||
throw reporter.createError('Failed to parse pptx template input', { | ||
original: parseTemplateError | ||
}) | ||
} | ||
} | ||
await preprocess(files) | ||
for (const f of files) { | ||
if (contentIsXML(f.data)) { | ||
f.doc = new DOMParser().parseFromString(f.data.toString()) | ||
f.data = f.data.toString() | ||
} | ||
} | ||
const filesToRender = files.filter(f => contentIsXML(f.data)) | ||
await preprocess(files) | ||
const contentToRender = ( | ||
filesToRender | ||
.map(f => new XMLSerializer().serializeToString(f.doc).replace(/<pptxRemove>/g, '').replace(/<\/pptxRemove>/g, '')) | ||
.join('$$$docxFile$$$') | ||
) | ||
const filesToRender = files.filter(f => contentIsXML(f.data)) | ||
reporter.logger.debug('Starting child request to render pptx dynamic parts', req) | ||
const contentToRender = ( | ||
filesToRender | ||
.map(f => new XMLSerializer().serializeToString(f.doc).replace(/<pptxRemove>/g, '').replace(/<\/pptxRemove>/g, '')) | ||
.join('$$$docxFile$$$') | ||
) | ||
const { content: newContent } = await reporter.render({ | ||
template: { | ||
content: contentToRender, | ||
engine: req.template.engine, | ||
recipe: 'html', | ||
helpers: req.template.helpers | ||
} | ||
}, req) | ||
reporter.logger.debug('Starting child request to render pptx dynamic parts', req) | ||
const contents = newContent.toString().split('$$$docxFile$$$') | ||
const { content: newContent } = await reporter.render({ | ||
template: { | ||
content: contentToRender, | ||
engine: req.template.engine, | ||
recipe: 'html', | ||
helpers: req.template.helpers | ||
} | ||
}, req) | ||
for (let i = 0; i < filesToRender.length; i++) { | ||
filesToRender[i].data = contents[i] | ||
filesToRender[i].doc = new DOMParser().parseFromString(contents[i]) | ||
} | ||
const contents = newContent.toString().split('$$$docxFile$$$') | ||
await postprocess(files) | ||
for (let i = 0; i < filesToRender.length; i++) { | ||
filesToRender[i].data = contents[i] | ||
filesToRender[i].doc = new DOMParser().parseFromString(contents[i]) | ||
} | ||
for (const f of files) { | ||
let isXML = false | ||
await postprocess(files) | ||
if (f.data == null) { | ||
isXML = f.path.includes('.xml') | ||
} else { | ||
isXML = contentIsXML(f.data) | ||
} | ||
for (const f of files) { | ||
let isXML = false | ||
if (isXML) { | ||
f.data = Buffer.from(new XMLSerializer().serializeToString(f.doc)) | ||
if (f.data == null) { | ||
isXML = f.path.includes('.xml') | ||
} else { | ||
isXML = contentIsXML(f.data) | ||
} | ||
if (isXML) { | ||
f.data = Buffer.from(new XMLSerializer().serializeToString(f.doc)) | ||
} | ||
} | ||
} | ||
await saveXmlsToOfficeFile({ | ||
outputPath, | ||
files | ||
}) | ||
await saveXmlsToOfficeFile({ | ||
outputPath, | ||
files | ||
}) | ||
reporter.logger.debug('pptx successfully zipped', req) | ||
reporter.logger.debug('pptx successfully zipped', req) | ||
return { | ||
pptxFilePath: outputPath | ||
return { | ||
pptxFilePath: outputPath | ||
} | ||
} catch (e) { | ||
throw reporter.createError('Error while executing pptx recipe', { | ||
original: e, | ||
weak: true | ||
}) | ||
} | ||
} |
@@ -0,14 +1,4 @@ | ||
const { nodeListToArray } = require('../utils') | ||
const regexp = /{{#?pptxTable [^{}]{0,500}}}/ | ||
const regexp = /{{#pptxTable [^{}]{0,500}}}/ | ||
function processClosingTag (doc, el) { | ||
el.textContent = el.textContent.replace('{{/pptxTable}}', '') | ||
const wpElement = el.parentNode.parentNode.parentNode.parentNode | ||
const fakeElement = doc.createElement('docxRemove') | ||
fakeElement.textContent = '{{/pptxTable}}' | ||
wpElement.parentNode.insertBefore(fakeElement, wpElement.nextSibling) | ||
} | ||
// the same idea as list, check the docs there | ||
@@ -18,27 +8,208 @@ module.exports = (files) => { | ||
const doc = f.doc | ||
const elements = doc.getElementsByTagName('w:t') | ||
const elements = doc.getElementsByTagName('a:t') | ||
const openTags = [] | ||
let openedDocx = false | ||
for (let i = 0; i < elements.length; i++) { | ||
const el = elements[i] | ||
if (el.textContent.includes('{{/pptxTable}}') && openedDocx) { | ||
openedDocx = false | ||
processClosingTag(doc, el) | ||
if (el.textContent.includes('{{/pptxTable}}') && openTags.length > 0) { | ||
const tag = openTags.pop() | ||
processClosingTag(doc, el, tag.mode === 'column') | ||
} | ||
if (el.textContent.includes('{{#pptxTable')) { | ||
if ( | ||
( | ||
el.textContent.includes('{{pptxTable') || | ||
el.textContent.includes('{{#pptxTable') | ||
) && | ||
el.textContent.includes('rows=') && | ||
el.textContent.includes('columns=') | ||
) { | ||
const isBlock = el.textContent.includes('{{#pptxTable') | ||
// full table mode | ||
let helperCall = el.textContent.match(regexp)[0] | ||
if (!isBlock) { | ||
// setting the cell text to be the value for the rows (before we clone) | ||
el.textContent = el.textContent.replace(regexp, '{{this}}') | ||
} else { | ||
el.textContent = el.textContent.replace(regexp, '') | ||
} | ||
const paragraphNode = el.parentNode.parentNode | ||
let newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{#if @placeholderCell}}' | ||
paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode) | ||
const emptyParagraphNode = paragraphNode.cloneNode(true) | ||
while (emptyParagraphNode.firstChild) { | ||
emptyParagraphNode.removeChild(emptyParagraphNode.firstChild) | ||
emptyParagraphNode.removeAttribute('__block_helper_container__') | ||
} | ||
paragraphNode.parentNode.parentNode.insertBefore(emptyParagraphNode, paragraphNode) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{else}}' | ||
paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{/if}}' | ||
paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode.nextSibling) | ||
const cellNode = paragraphNode.parentNode.parentNode | ||
const cellPropertiesNode = nodeListToArray(cellNode.childNodes).find((node) => node.nodeName === 'a:tcPr') | ||
// insert conditional logic for colspan and rowspan | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{#pptxTable check="colspan"}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('a:gridSpan') | ||
newElement.setAttribute('a:val', '{{this}}') | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{/pptxTable}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{#pptxTable check="rowspan"}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{#if @empty}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('a:vMerge') | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{else}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('a:vMerge') | ||
newElement.setAttribute('a:val', 'restart') | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{/if}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{/pptxTable}}' | ||
cellPropertiesNode.appendChild(newElement) | ||
const rowNode = cellNode.parentNode | ||
const tableNode = rowNode.parentNode | ||
const newRowNode = rowNode.cloneNode(true) | ||
if (!isBlock) { | ||
helperCall = helperCall.replace('{{pptxTable', '{{#pptxTable') | ||
} | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = helperCall.replace('{{#pptxTable', '{{#pptxTable wrapper="main"') | ||
tableNode.parentNode.insertBefore(newElement, tableNode) | ||
newElement = doc.createElement('pptxRemove') | ||
newElement.textContent = '{{/pptxTable}}' | ||
tableNode.parentNode.insertBefore(newElement, tableNode.nextSibling) | ||
if (isBlock) { | ||
openTags.push({ mode: 'column' }) | ||
} | ||
processOpeningTag(doc, cellNode, helperCall.replace('rows=', 'ignore=')) | ||
if (!isBlock) { | ||
processClosingTag(doc, cellNode) | ||
} else { | ||
if (el.textContent.includes('{{/pptxTable')) { | ||
openTags.pop() | ||
processClosingTag(doc, el, true) | ||
} | ||
const clonedTextNodes = nodeListToArray(newRowNode.getElementsByTagName('a:t')) | ||
for (const tNode of clonedTextNodes) { | ||
if (tNode.textContent.includes('{{/pptxTable')) { | ||
tNode.textContent = tNode.textContent.replace('{{/pptxTable}}', '') | ||
} | ||
} | ||
} | ||
// row template, handling the cells for the data values | ||
rowNode.parentNode.insertBefore(newRowNode, rowNode.nextSibling) | ||
const cellInNewRowNode = nodeListToArray(newRowNode.childNodes).find((node) => node.nodeName === 'a:tc') | ||
processOpeningTag(doc, cellInNewRowNode, helperCall.replace('rows=', 'ignore=').replace('columns=', 'ignore=')) | ||
processClosingTag(doc, cellInNewRowNode) | ||
processOpeningTag(doc, newRowNode, helperCall) | ||
processClosingTag(doc, newRowNode) | ||
} else if (el.textContent.includes('{{#pptxTable')) { | ||
const helperCall = el.textContent.match(regexp)[0] | ||
const wpElement = el.parentNode.parentNode.parentNode.parentNode | ||
const fakeElement = doc.createElement('docxRemove') | ||
fakeElement.textContent = helperCall | ||
const isVertical = el.textContent.includes('vertical=') | ||
const isNormal = !isVertical | ||
wpElement.parentNode.insertBefore(fakeElement, wpElement) | ||
el.textContent = el.textContent.replace(regexp, '') | ||
if (el.textContent.includes('{{/pptxTable')) { | ||
processClosingTag(doc, el) | ||
if (isNormal) { | ||
openTags.push({ mode: 'row' }) | ||
} | ||
if (isVertical) { | ||
const cellNode = el.parentNode.parentNode.parentNode.parentNode | ||
const cellIndex = getCellIndex(cellNode) | ||
const [affectedRows, textNodeTableClose] = getNextRowsUntilTableClose(cellNode.parentNode) | ||
if (textNodeTableClose) { | ||
textNodeTableClose.textContent = textNodeTableClose.textContent.replace('{{/pptxTable}}', '') | ||
} | ||
const tableNode = cellNode.parentNode.parentNode | ||
const tableGridNode = tableNode.getElementsByTagName('a:tblGrid')[0] | ||
const tableGridColNodes = nodeListToArray(tableGridNode.getElementsByTagName('a:gridCol')) | ||
// add loop for colum definitions (pptx table requires this to show the newly created columns) | ||
processOpeningTag(doc, tableGridColNodes[cellIndex], helperCall, isVertical) | ||
processClosingTag(doc, tableGridColNodes[cellIndex], isVertical) | ||
processOpeningTag(doc, el, helperCall, isVertical) | ||
processClosingTag(doc, el, isVertical) | ||
for (const rowNode of affectedRows) { | ||
const cellNodes = nodeListToArray(rowNode.childNodes).filter((node) => node.nodeName === 'a:tc') | ||
const cellNode = cellNodes[cellIndex] | ||
if (cellNode) { | ||
processOpeningTag(doc, cellNode, helperCall, isVertical) | ||
processClosingTag(doc, cellNode, isVertical) | ||
} | ||
} | ||
} else { | ||
openedDocx = true | ||
processOpeningTag(doc, el, helperCall, isVertical) | ||
} | ||
if (isNormal && el.textContent.includes('{{/pptxTable')) { | ||
openTags.pop() | ||
processClosingTag(doc, el) | ||
} | ||
} | ||
@@ -48,1 +219,124 @@ } | ||
} | ||
function processOpeningTag (doc, el, helperCall, useColumnRef = false) { | ||
if (el.nodeName === 'a:t') { | ||
el.textContent = el.textContent.replace(regexp, '') | ||
} | ||
const fakeElement = doc.createElement('pptxRemove') | ||
fakeElement.textContent = helperCall | ||
let refElement | ||
if (el.nodeName !== 'a:t') { | ||
refElement = el | ||
} else { | ||
if (useColumnRef) { | ||
// ref is the column a:tc | ||
refElement = el.parentNode.parentNode.parentNode.parentNode | ||
} else { | ||
// ref is the row a:tr | ||
refElement = el.parentNode.parentNode.parentNode.parentNode.parentNode | ||
} | ||
} | ||
refElement.parentNode.insertBefore(fakeElement, refElement) | ||
} | ||
function processClosingTag (doc, el, useColumnRef = false) { | ||
if (el.nodeName === 'a:t') { | ||
el.textContent = el.textContent.replace('{{/pptxTable}}', '') | ||
} | ||
const fakeElement = doc.createElement('pptxRemove') | ||
fakeElement.textContent = '{{/pptxTable}}' | ||
let refElement | ||
if (el.nodeName !== 'a:t') { | ||
refElement = el | ||
} else { | ||
if (useColumnRef) { | ||
refElement = el.parentNode.parentNode.parentNode.parentNode | ||
} else { | ||
refElement = el.parentNode.parentNode.parentNode.parentNode.parentNode | ||
} | ||
} | ||
refElement.parentNode.insertBefore(fakeElement, refElement.nextSibling) | ||
} | ||
function getCellIndex (cellEl) { | ||
if (cellEl.nodeName !== 'a:tc') { | ||
throw new Error('Expected a table cell element during the processing') | ||
} | ||
let prevElements = 0 | ||
let currentNode = cellEl.previousSibling | ||
while ( | ||
currentNode != null && | ||
currentNode.nodeName === 'a:tc' | ||
) { | ||
prevElements += 1 | ||
currentNode = currentNode.previousSibling | ||
} | ||
return prevElements | ||
} | ||
function getNextRowsUntilTableClose (rowEl) { | ||
if (rowEl.nodeName !== 'a:tr') { | ||
throw new Error('Expected a table row element during the processing') | ||
} | ||
let currentNode = rowEl.nextSibling | ||
let tableCloseNode | ||
const rows = [] | ||
while ( | ||
currentNode != null && | ||
currentNode.nodeName === 'a:tr' | ||
) { | ||
rows.push(currentNode) | ||
const cellNodes = nodeListToArray(currentNode.childNodes).filter((node) => node.nodeName === 'a:tc') | ||
for (const cellNode of cellNodes) { | ||
let textNodes = nodeListToArray(cellNode.getElementsByTagName('a:t')) | ||
// get text nodes of the current cell, we don't want text | ||
// nodes of nested tables | ||
textNodes = textNodes.filter((tNode) => { | ||
let current = tNode.parentNode | ||
while (current.nodeName !== 'a:tc') { | ||
current = current.parentNode | ||
} | ||
return current === cellNode | ||
}) | ||
for (const tNode of textNodes) { | ||
if (tNode.textContent.includes('{{/pptxTable')) { | ||
currentNode = null | ||
tableCloseNode = tNode | ||
break | ||
} | ||
} | ||
if (currentNode == null) { | ||
break | ||
} | ||
} | ||
if (currentNode != null) { | ||
currentNode = currentNode.nextSibling | ||
} | ||
} | ||
return [rows, tableCloseNode] | ||
} |
@@ -30,3 +30,3 @@ const fs = require('fs') | ||
if (!Buffer.isBuffer(templateAsset.content)) { | ||
templateAsset.content = Buffer.from(templateAsset.content, templateAsset.encoding || 'utf8') | ||
templateAsset.content = Buffer.from(templateAsset.content, templateAsset.encoding || 'base64') | ||
} | ||
@@ -33,0 +33,0 @@ } |
function pxToEMU (val) { | ||
return Math.round(val * 914400 / 96) | ||
} | ||
function cmToEMU (val) { | ||
// cm to dxa | ||
// eslint-disable-next-line no-loss-of-precision | ||
const dxa = val * 567.058823529411765 | ||
// dxa to EMU | ||
return Math.round(dxa * 914400 / 72 / 20) | ||
} | ||
module.exports.contentIsXML = (content) => { | ||
@@ -19,1 +31,4 @@ if (!Buffer.isBuffer(content) && typeof content !== 'string') { | ||
} | ||
module.exports.pxToEMU = pxToEMU | ||
module.exports.cmToEMU = cmToEMU |
{ | ||
"name": "@jsreport/jsreport-pptx", | ||
"version": "3.2.1", | ||
"version": "3.3.0", | ||
"description": "jsreport recipe rendering pptx files", | ||
@@ -37,12 +37,14 @@ "keywords": [ | ||
"@jsreport/office": "3.0.0", | ||
"@xmldom/xmldom": "0.7.0", | ||
"@xmldom/xmldom": "0.8.6", | ||
"axios": "0.24.0", | ||
"image-size": "0.7.4" | ||
}, | ||
"devDependencies": { | ||
"@jsreport/jsreport-assets": "3.4.2", | ||
"@jsreport/jsreport-core": "3.6.0", | ||
"@jsreport/jsreport-handlebars": "3.1.0", | ||
"@jsreport/studio-dev": "3.1.0", | ||
"@jsreport/jsreport-assets": "3.5.0", | ||
"@jsreport/jsreport-core": "3.9.0", | ||
"@jsreport/jsreport-handlebars": "3.2.1", | ||
"@jsreport/studio-dev": "3.2.0", | ||
"handlebars": "4.7.7", | ||
"mocha": "6.1.4", | ||
"mocha": "10.1.0", | ||
"nock": "11.7.2", | ||
"should": "13.2.3", | ||
@@ -49,0 +51,0 @@ "standard": "16.0.4", |
@@ -10,2 +10,8 @@ # @jsreport/jsreport-pptx | ||
### 3.3.0 | ||
- fix support of pptxTable and add support for vertical tables | ||
- pptxImage now support same options as `docxImage` (usePlaceholderSize, width, height options) | ||
- accept buffer strings as base64 and throw better error when failed to parse office template input | ||
### 3.2.1 | ||
@@ -12,0 +18,0 @@ |
@@ -11,3 +11,214 @@ /* eslint no-unused-vars: 0 */ | ||
const Handlebars = require('handlebars') | ||
return Handlebars.helpers.each(data, options) | ||
const optionsToUse = options == null ? data : options | ||
let currentData | ||
const getMatchedMergedCell = (rowIndex, columnIndex, activeMergedCellsItems) => { | ||
let matchedRowspan | ||
for (const item of activeMergedCellsItems) { | ||
if ( | ||
rowIndex >= item.rowStart && | ||
rowIndex <= item.rowEnd && | ||
columnIndex >= item.colStart && | ||
columnIndex <= item.colEnd | ||
) { | ||
matchedRowspan = item | ||
break | ||
} | ||
} | ||
return matchedRowspan | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'wrapper') && | ||
optionsToUse.hash.wrapper === 'main' | ||
) { | ||
const newData = Handlebars.createFrame({}) | ||
newData.rows = optionsToUse.hash.rows | ||
newData.columns = optionsToUse.hash.columns | ||
newData.activeMergedCellsItems = [] | ||
return optionsToUse.fn(this, { data: newData }) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'check') | ||
) { | ||
if ( | ||
optionsToUse.hash.check === 'colspan' && | ||
optionsToUse.data.colspan > 1 | ||
) { | ||
return optionsToUse.fn(optionsToUse.data.colspan) | ||
} | ||
if ( | ||
optionsToUse.hash.check === 'rowspan' | ||
) { | ||
const matchedMergedCell = getMatchedMergedCell(optionsToUse.data.rowIndex, optionsToUse.data.columnIndex, optionsToUse.data.activeMergedCellsItems) | ||
if (matchedMergedCell != null && matchedMergedCell.rowStart !== matchedMergedCell.rowEnd) { | ||
const data = Handlebars.createFrame({}) | ||
data.empty = matchedMergedCell.rowStart !== optionsToUse.data.rowIndex | ||
return optionsToUse.fn({}, { data }) | ||
} | ||
} | ||
return new Handlebars.SafeString('') | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
( | ||
Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'rows') || | ||
Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'columns') || | ||
Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'ignore') | ||
) | ||
) { | ||
// full table mode | ||
if (Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'rows')) { | ||
if (!Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'columns')) { | ||
throw new Error('pptxTable full table mode needs to have both rows and columns defined as params when processing row') | ||
} | ||
// rows block processing start here | ||
currentData = optionsToUse.hash.rows | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
optionsToUse.data = newData | ||
const chunks = [] | ||
if (!currentData || !Array.isArray(currentData)) { | ||
return new Handlebars.SafeString('') | ||
} | ||
for (let i = 0; i < currentData.length; i++) { | ||
newData.index = i | ||
chunks.push(optionsToUse.fn(this, { data: newData })) | ||
} | ||
return new Handlebars.SafeString(chunks.join('')) | ||
} else { | ||
// columns processing, when isInsideRowHelper is false it means | ||
// that we are processing the first row based on columns info, | ||
// when true it means we are processing columns inside the rows block | ||
let isInsideRowHelper = false | ||
if (optionsToUse.hash.columns) { | ||
currentData = optionsToUse.hash.columns | ||
} else if (optionsToUse.data && optionsToUse.data.columns) { | ||
isInsideRowHelper = true | ||
currentData = optionsToUse.data.columns | ||
} else { | ||
throw new Error('pptxTable full table mode needs to have columns defined when processing column') | ||
} | ||
const chunks = [] | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
const rowIndex = newData.index || 0 | ||
delete newData.index | ||
delete newData.key | ||
delete newData.first | ||
delete newData.last | ||
if (!currentData || !Array.isArray(currentData)) { | ||
return new Handlebars.SafeString('') | ||
} | ||
const getCellInfo = (item) => { | ||
const cellInfo = {} | ||
if (item != null && typeof item === 'object' && !Array.isArray(item)) { | ||
cellInfo.value = item.value | ||
cellInfo.colspan = item.colspan | ||
cellInfo.rowspan = item.rowspan | ||
} else { | ||
cellInfo.value = item | ||
} | ||
if (cellInfo.colspan == null) { | ||
cellInfo.colspan = 1 | ||
} | ||
if (cellInfo.rowspan == null) { | ||
cellInfo.rowspan = 1 | ||
} | ||
return cellInfo | ||
} | ||
// if all cells in current row have the same rowspan then | ||
// assume there is no rowspan applied | ||
const cellsInRow = isInsideRowHelper ? optionsToUse.data.rows[rowIndex] : currentData | ||
for (const [idx, item] of cellsInRow.entries()) { | ||
// rowIndex + 1 because this is technically the second row on table after the row of table headers | ||
newData.rowIndex = isInsideRowHelper ? rowIndex + 1 : 0 | ||
newData.columnIndex = cellsInRow.reduce((acu, cell, cellIdx) => { | ||
if (cellIdx >= idx) { | ||
return acu | ||
} | ||
const matchedMergedCell = getMatchedMergedCell(newData.rowIndex, acu, newData.activeMergedCellsItems) | ||
if (matchedMergedCell != null && matchedMergedCell.colStart !== matchedMergedCell.colEnd) { | ||
return acu + (matchedMergedCell.colEnd - matchedMergedCell.colStart) + 1 | ||
} | ||
const cellInfo = getCellInfo(cell) | ||
return acu + cellInfo.colspan | ||
}, 0) | ||
const currentItem = isInsideRowHelper ? optionsToUse.data.rows[rowIndex][idx] : item | ||
const cellInfo = getCellInfo(currentItem) | ||
const allCellsInRowHaveSameRowspan = cellsInRow.every((cell) => { | ||
const cellInfo = getCellInfo(cell) | ||
return cellInfo.rowspan === getCellInfo(cellsInRow[0]).rowspan | ||
}) | ||
if (allCellsInRowHaveSameRowspan) { | ||
cellInfo.rowspan = 1 | ||
} | ||
newData.colspan = cellInfo.colspan | ||
newData.rowspan = cellInfo.rowspan | ||
if (newData.rowspan > 1 || newData.colspan > 1) { | ||
newData.activeMergedCellsItems.push({ | ||
colStart: newData.columnIndex, | ||
colEnd: newData.columnIndex + (newData.colspan - 1), | ||
rowStart: newData.rowIndex, | ||
rowEnd: newData.rowIndex + (newData.rowspan - 1) | ||
}) | ||
} | ||
const matchedMergedCell = getMatchedMergedCell(newData.rowIndex, newData.columnIndex, newData.activeMergedCellsItems) | ||
if ( | ||
matchedMergedCell != null && | ||
matchedMergedCell.rowStart !== matchedMergedCell.rowEnd && | ||
matchedMergedCell.rowStart !== newData.rowIndex | ||
) { | ||
newData.placeholderCell = true | ||
newData.colspan = (matchedMergedCell.colEnd - matchedMergedCell.colStart) + 1 | ||
} else { | ||
newData.placeholderCell = false | ||
} | ||
chunks.push(optionsToUse.fn(cellInfo.value, { data: newData })) | ||
} | ||
return new Handlebars.SafeString(chunks.join('')) | ||
} | ||
} else { | ||
currentData = data | ||
} | ||
return Handlebars.helpers.each(currentData, optionsToUse) | ||
} | ||
@@ -22,3 +233,58 @@ | ||
const Handlebars = require('handlebars') | ||
return new Handlebars.SafeString(`<pptxImage src="${options.hash.src}" />`) | ||
if (!options.hash.src) { | ||
throw new Error( | ||
'pptxImage helper requires src parameter to be set' | ||
) | ||
} | ||
if ( | ||
!options.hash.src.startsWith('data:image/png;base64,') && | ||
!options.hash.src.startsWith('data:image/jpeg;base64,') && | ||
!options.hash.src.startsWith('http://') && | ||
!options.hash.src.startsWith('https://') | ||
) { | ||
throw new Error( | ||
'pptxImage helper requires src parameter to be valid data uri for png or jpeg image or a valid url. Got ' + | ||
options.hash.src | ||
) | ||
} | ||
const isValidDimensionUnit = value => { | ||
const regexp = /^(\d+(.\d+)?)(cm|px)$/ | ||
return regexp.test(value) | ||
} | ||
if ( | ||
options.hash.width != null && | ||
!isValidDimensionUnit(options.hash.width) | ||
) { | ||
throw new Error( | ||
'pptxImage helper requires width parameter to be valid number with unit (cm or px). got ' + | ||
options.hash.width | ||
) | ||
} | ||
if ( | ||
options.hash.height != null && | ||
!isValidDimensionUnit(options.hash.height) | ||
) { | ||
throw new Error( | ||
'pptxImage helper requires height parameter to be valid number with unit (cm or px). got ' + | ||
options.hash.height | ||
) | ||
} | ||
const content = `$pptxImage${ | ||
Buffer.from(JSON.stringify({ | ||
src: options.hash.src, | ||
width: options.hash.width, | ||
height: options.hash.height, | ||
usePlaceholderSize: | ||
options.hash.usePlaceholderSize === true || | ||
options.hash.usePlaceholderSize === 'true' | ||
})).toString('base64') | ||
}$` | ||
return new Handlebars.SafeString(`<pptxImage>${content}</pptxImage>`) | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77265
1482
33
4
10
+ Addedaxios@0.24.0
+ Added@xmldom/xmldom@0.8.6(transitive)
+ Addedaxios@0.24.0(transitive)
- Removed@xmldom/xmldom@0.7.0(transitive)
Updated@xmldom/xmldom@0.8.6