diff --git a/examples/pdfs/sections.pdf b/examples/pdfs/sections.pdf new file mode 100644 index 000000000..b26e2f244 Binary files /dev/null and b/examples/pdfs/sections.pdf differ diff --git a/examples/sections.js b/examples/sections.js new file mode 100644 index 000000000..3f25acc54 --- /dev/null +++ b/examples/sections.js @@ -0,0 +1,90 @@ +var pdfmake = require('../js/index'); // only during development, otherwise use the following line +//var pdfmake = require('pdfmake'); + +var Roboto = require('../fonts/Roboto'); +pdfmake.addFonts(Roboto); + +var docDefinition = { + header: function () { return 'default header'; }, + footer: function () { return 'default footer'; }, + background: function() { return { text:'global background', alignment: 'right' }; }, + watermark: 'default watermark', + content: [ + { + section: [ + 'SECTION 1', + 'Text in section.' + ] + }, + { + header: function (currentPage, pageCount) { return 'header: ' + currentPage.toString() + ' of ' + pageCount; }, + footer: function (currentPage, pageCount) { return 'footer: ' + currentPage.toString() + ' of ' + pageCount; }, + background: function() { return { text:'SECTION 2 background', alignment: 'right' }; }, + watermark: 'SECTION 2 watermark', + pageOrientation: 'landscape', + section: [ + 'SECTION 2', + 'Text in section as landscape page.' + ] + }, + { + header: null, + footer: null, + background: null, + watermark: null, + pageSize: 'A7', + pageOrientation: 'portrait', + section: [ + 'SECTION 3', + 'Text in section as A7 page.' + ] + }, + { + watermark: 'inherit', + pageSize: 'A6', + pageOrientation: 'portrait', + pageMargins: 5, + section: [ + 'SECTION 4', + 'Text in section as A6 page with margin.' + ] + }, + { + watermark: 'watermark for inherit', + pageSize: 'A6', + pageOrientation: 'landscape', + pageMargins: 10, + section: [ + 'SECTION 5', + 'Text in section as A6 landscape page with margin.' + ] + }, + { + watermark: 'inherit', + pageSize: 'inherit', + pageOrientation: 'inherit', + pageMargins: 'inherit', + section: [ + 'SECTION 6', + 'Text in section with page definition as previous page. Page size, orientation and margins are inherited.' + ] + }, + { + header: function (currentPage, pageCount) { return 'header in section 8: ' + currentPage.toString() + ' of ' + pageCount; }, + footer: function (currentPage, pageCount) { return 'footer in section 8: ' + currentPage.toString() + ' of ' + pageCount; }, + section: [ + 'SECTION 7', + 'Text in section with page definition as defined in document.' + ] + } + ] +}; + +var now = new Date(); + +var pdf = pdfmake.createPdf(docDefinition); +pdf.write('pdfs/sections.pdf').then(() => { + console.log(new Date() - now); +}, err => { + console.error(err); +}); diff --git a/src/DocMeasure.js b/src/DocMeasure.js index 79eeebbcf..c300e49ab 100644 --- a/src/DocMeasure.js +++ b/src/DocMeasure.js @@ -34,12 +34,18 @@ class DocMeasure { return this.measureNode(docStructure); } + measureBlock(node) { + return this.measureNode(node); + } + measureNode(node) { return this.styleStack.auto(node, () => { // TODO: refactor + rethink whether this is the proper way to handle margins node._margin = getNodeMargin(node, this.styleStack); - if (node.columns) { + if (node.section) { + return extendMargins(this.measureSection(node)); + } else if (node.columns) { return extendMargins(this.measureColumns(node)); } else if (node.stack) { return extendMargins(this.measureVerticalContainer(node)); @@ -465,6 +471,14 @@ class DocMeasure { return node; } + measureSection(node) { + // TODO: properties + + node.section = this.measureNode(node.section); + + return node; + } + measureColumns(node) { let columns = node.columns; node._gap = this.styleStack.getProperty('columnGap') || 0; diff --git a/src/DocPreprocessor.js b/src/DocPreprocessor.js index 6002555b3..979c3c822 100644 --- a/src/DocPreprocessor.js +++ b/src/DocPreprocessor.js @@ -20,10 +20,17 @@ class DocPreprocessor { this.parentNode = null; this.tocs = []; this.nodeReferences = []; - return this.preprocessNode(docStructure); + return this.preprocessNode(docStructure, true); } - preprocessNode(node) { + preprocessBlock(node) { + this.parentNode = null; + this.tocs = []; + this.nodeReferences = []; + return this.preprocessNode(node); + } + + preprocessNode(node, isSectionAllowed = false) { // expand shortcuts and casting values if (Array.isArray(node)) { node = { stack: node }; @@ -33,10 +40,16 @@ class DocPreprocessor { node.text = convertValueToString(node.text); } - if (node.columns) { + if (node.section) { + if (!isSectionAllowed) { + throw new Error(`Incorrect document structure, section node is only allowed at the root level of document structure: ${stringifyNode(node)}`); + } + + return this.preprocessSection(node); + } else if (node.columns) { return this.preprocessColumns(node); } else if (node.stack) { - return this.preprocessVerticalContainer(node); + return this.preprocessVerticalContainer(node, isSectionAllowed); } else if (node.ul) { return this.preprocessList(node); } else if (node.ol) { @@ -64,6 +77,12 @@ class DocPreprocessor { } } + preprocessSection(node) { + node.section = this.preprocessNode(node.section); + + return node; + } + preprocessColumns(node) { let columns = node.columns; @@ -74,11 +93,11 @@ class DocPreprocessor { return node; } - preprocessVerticalContainer(node) { + preprocessVerticalContainer(node, isSectionAllowed) { let items = node.stack; for (let i = 0, l = items.length; i < l; i++) { - items[i] = this.preprocessNode(items[i]); + items[i] = this.preprocessNode(items[i], isSectionAllowed); } return node; diff --git a/src/DocumentContext.js b/src/DocumentContext.js index 11b414372..899816415 100644 --- a/src/DocumentContext.js +++ b/src/DocumentContext.js @@ -222,7 +222,7 @@ class DocumentContext extends EventEmitter { let currentPageOrientation = this.getCurrentPage().pageSize.orientation; let pageSize = getPageSize(this.getCurrentPage(), pageOrientation); - this.addPage(pageSize); + this.addPage(pageSize, null, this.getCurrentPage().customProperties); if (currentPageOrientation === pageSize.orientation) { this.availableWidth = currentAvailableWidth; @@ -240,20 +240,20 @@ class DocumentContext extends EventEmitter { }; } - addPage(pageSize, pageMargin = null) { + addPage(pageSize, pageMargin = null, customProperties = {}) { if (pageMargin !== null) { this.pageMargins = pageMargin; this.x = pageMargin.left; this.availableWidth = pageSize.width - pageMargin.left - pageMargin.right; } - let page = { items: [], pageSize: pageSize, pageMargins: this.pageMargins }; + let page = { items: [], pageSize: pageSize, pageMargins: this.pageMargins, customProperties: customProperties }; this.pages.push(page); this.backgroundLength.push(0); this.page = this.pages.length - 1; this.initializePage(); - this.emit('pageAdded'); + this.emit('pageAdded', page); return page; } diff --git a/src/LayoutBuilder.js b/src/LayoutBuilder.js index 1dc884a28..92be44507 100644 --- a/src/LayoutBuilder.js +++ b/src/LayoutBuilder.js @@ -7,7 +7,7 @@ import TableProcessor from './TableProcessor'; import Line from './Line'; import { isString, isValue, isNumber } from './helpers/variableType'; import { stringifyNode, getNodeId } from './helpers/node'; -import { pack, offsetVector } from './helpers/tools'; +import { pack, offsetVector, convertToDynamicContent } from './helpers/tools'; import TextInlines from './TextInlines'; import StyleContextStack from './StyleContextStack'; @@ -168,27 +168,42 @@ class LayoutBuilder { watermark ) { + const isNecessaryAddFirstPage = (docStructure) => { + if (docStructure.stack && docStructure.stack.length > 0 && docStructure.stack[0].section) { + return false; + } else if (docStructure.section) { + return false; + } + + return true; + }; + this.linearNodeList = []; docStructure = this.docPreprocessor.preprocessDocument(docStructure); docStructure = this.docMeasure.measureDocument(docStructure); this.writer = new PageElementWriter(new DocumentContext()); - this.writer.context().addListener('pageAdded', () => { - this.addBackground(background); + this.writer.context().addListener('pageAdded', (page) => { + let backgroundGetter = background; + if (page.customProperties['background'] || page.customProperties['background'] === null) { + backgroundGetter = page.customProperties['background']; + } + + this.addBackground(backgroundGetter); }); - this.writer.addPage( - this.pageSize, - null, - this.pageMargins - ); + if (isNecessaryAddFirstPage(docStructure)) { + this.writer.addPage( + this.pageSize, + null, + this.pageMargins + ); + } this.processNode(docStructure); this.addHeadersAndFooters(header, footer); - if (watermark != null) { - this.addWatermark(watermark, pdfDocument, defaultStyle); - } + this.addWatermark(watermark, pdfDocument, defaultStyle); return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList }; } @@ -203,26 +218,37 @@ class LayoutBuilder { if (pageBackground) { this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height); - pageBackground = this.docPreprocessor.preprocessDocument(pageBackground); - this.processNode(this.docMeasure.measureDocument(pageBackground)); + pageBackground = this.docPreprocessor.preprocessBlock(pageBackground); + this.processNode(this.docMeasure.measureBlock(pageBackground)); this.writer.commitUnbreakableBlock(0, 0); context.backgroundLength[context.page] += pageBackground.positions.length; } } - addDynamicRepeatable(nodeGetter, sizeFunction) { + addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) { let pages = this.writer.context().pages; for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) { this.writer.context().page = pageIndex; - let node = nodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize); + let customProperties = this.writer.context().getCurrentPage().customProperties; + + let pageNodeGetter = nodeGetter; + if (customProperties[customPropertyName] || customProperties[customPropertyName] === null) { + pageNodeGetter = customProperties[customPropertyName]; + } + + if ((typeof pageNodeGetter === 'undefined') || (pageNodeGetter === null)) { + continue; + } + + let node = pageNodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize); if (node) { let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins); this.writer.beginUnbreakableBlock(sizes.width, sizes.height); - node = this.docPreprocessor.preprocessDocument(node); - this.processNode(this.docMeasure.measureDocument(node)); + node = this.docPreprocessor.preprocessBlock(node); + this.processNode(this.docMeasure.measureBlock(node)); this.writer.commitUnbreakableBlock(sizes.x, sizes.y); } } @@ -243,27 +269,31 @@ class LayoutBuilder { height: pageMargins.bottom }); - if (header) { - this.addDynamicRepeatable(header, headerSizeFct); - } - - if (footer) { - this.addDynamicRepeatable(footer, footerSizeFct); - } + this.addDynamicRepeatable(header, headerSizeFct, 'header'); + this.addDynamicRepeatable(footer, footerSizeFct, 'footer'); } addWatermark(watermark, pdfDocument, defaultStyle) { - if (isString(watermark)) { - watermark = { 'text': watermark }; - } - - if (!watermark.text) { // empty watermark text - return; - } - let pages = this.writer.context().pages; for (let i = 0, l = pages.length; i < l; i++) { - pages[i].watermark = getWatermarkObject({ ...watermark }, pages[i].pageSize, pdfDocument, defaultStyle); + let pageWatermark = watermark; + if (pages[i].customProperties['watermark'] || pages[i].customProperties['watermark'] === null) { + pageWatermark = pages[i].customProperties['watermark']; + } + + if (pageWatermark === undefined || pageWatermark === null) { + continue; + } + + if (isString(pageWatermark)) { + pageWatermark = { 'text': pageWatermark }; + } + + if (!pageWatermark.text) { // empty watermark text + continue; + } + + pages[i].watermark = getWatermarkObject({ ...pageWatermark }, pages[i].pageSize, pdfDocument, defaultStyle); } function getWatermarkObject(watermark, pageSize, pdfDocument, defaultStyle) { @@ -453,6 +483,8 @@ class LayoutBuilder { if (node.stack) { this.processVerticalContainer(node); + } else if (node.section) { + this.processSection(node); } else if (node.columns) { this.processColumns(node); } else if (node.ul) { @@ -499,6 +531,75 @@ class LayoutBuilder { }, this); } + // section + processSection(sectionNode) { + // TODO: properties + + let page = this.writer.context().getCurrentPage(); + if (!page || (page && page.items.length)) { // move to new empty page + // page definition inherit from current page + if (sectionNode.pageSize === 'inherit') { + sectionNode.pageSize = page ? { width: page.pageSize.width, height: page.pageSize.height } : undefined; + } + if (sectionNode.pageOrientation === 'inherit') { + sectionNode.pageOrientation = page ? page.pageSize.orientation : undefined; + } + if (sectionNode.pageMargins === 'inherit') { + sectionNode.pageMargins = page ? page.pageMargins : undefined; + } + + if (sectionNode.header === 'inherit') { + sectionNode.header = page ? page.customProperties.header : undefined; + } + + if (sectionNode.footer === 'inherit') { + sectionNode.footer = page ? page.customProperties.footer : undefined; + } + + if (sectionNode.background === 'inherit') { + sectionNode.background = page ? page.customProperties.background : undefined; + } + + if (sectionNode.watermark === 'inherit') { + sectionNode.watermark = page ? page.customProperties.watermark : undefined; + } + + if (sectionNode.header && typeof sectionNode.header !== 'function' && sectionNode.header !== null) { + sectionNode.header = convertToDynamicContent(sectionNode.header); + } + + if (sectionNode.footer && typeof sectionNode.footer !== 'function' && sectionNode.footer !== null) { + sectionNode.footer = convertToDynamicContent(sectionNode.footer); + } + + let customProperties = {}; + if (typeof sectionNode.header !== 'undefined') { + customProperties.header = sectionNode.header; + } + + if (typeof sectionNode.footer !== 'undefined') { + customProperties.footer = sectionNode.footer; + } + + if (typeof sectionNode.background !== 'undefined') { + customProperties.background = sectionNode.background; + } + + if (typeof sectionNode.watermark !== 'undefined') { + customProperties.watermark = sectionNode.watermark; + } + + this.writer.addPage( + sectionNode.pageSize || this.pageSize, + sectionNode.pageOrientation, + sectionNode.pageMargins || this.pageMargins, + customProperties + ); + } + + this.processNode(sectionNode.section); + } + // columns processColumns(columnNode) { this.nestedLevel++; diff --git a/src/PageElementWriter.js b/src/PageElementWriter.js index 97fc2047c..9582f8a7d 100644 --- a/src/PageElementWriter.js +++ b/src/PageElementWriter.js @@ -83,11 +83,11 @@ class PageElementWriter extends ElementWriter { }); } - addPage(pageSize, pageOrientation, pageMargin) { + addPage(pageSize, pageOrientation, pageMargin, customProperties = {}) { let prevPage = this.page; let prevY = this.y; - this.context().addPage(normalizePageSize(pageSize, pageOrientation), normalizePageMargin(pageMargin)); + this.context().addPage(normalizePageSize(pageSize, pageOrientation), normalizePageMargin(pageMargin), customProperties); this.emit('pageChanged', { prevPage: prevPage, diff --git a/src/StyleContextStack.js b/src/StyleContextStack.js index 188fbef0e..b2bf59270 100644 --- a/src/StyleContextStack.js +++ b/src/StyleContextStack.js @@ -65,6 +65,10 @@ class StyleContextStack { return 0; } + if (typeof item.section !== 'undefined') { // section node not support style overrides + return 0; + } + let styleNames = []; if (item.style) { diff --git a/tests/unit/DocPreprocessor.spec.js b/tests/unit/DocPreprocessor.spec.js index 15f088fb0..ff7decebc 100644 --- a/tests/unit/DocPreprocessor.spec.js +++ b/tests/unit/DocPreprocessor.spec.js @@ -1,4 +1,3 @@ - const assert = require('assert'); const DocPreprocessor = require('../../js/DocPreprocessor').default; @@ -239,4 +238,54 @@ describe('DocPreprocessor', function () { }); + describe('section', function () { + + it('should support section', function () { + var ddContent = [ + { + section: [], + }, + { + section: [], + }, + ]; + assert.doesNotThrow(function () { + docPreprocessor.preprocessDocument(ddContent); + }); + }); + + it('should support section in stack', function () { + var ddContent = [ + { + stack: [ + { + section: [], + }, + { + section: [], + }, + ] + } + ]; + assert.doesNotThrow(function () { + docPreprocessor.preprocessDocument(ddContent); + }); + }); + + it('should support section only in root', function () { + var ddContent = [ + { + table: { + body: [ + [{ section: [] }], + ] + } + }, + ]; + + assert.throws(() => docPreprocessor.preprocessDocument(ddContent), /Incorrect document structure, section node is only allowed at the root level of document structure/); + }); + + }); + });