From 2ee43ed954b94d9b18fcb8f421acc07e911cd473 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 03:55:46 +0300 Subject: [PATCH 01/18] set lockAddPage to false --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 6344638..7805b0c 100644 --- a/index.js +++ b/index.js @@ -483,7 +483,7 @@ class PDFDocumentWithTables extends PDFDocument { this.logg('CRAZY! This a big text on cell'); } else if(calc > maxY) { // && !lockAddPage // lockAddHeader = false; - lockAddPage = true; + lockAddPage = false; onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); return; } From c8e1033ca1daac7cb4acc5bff54729871d73d49f Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 10:21:39 +0300 Subject: [PATCH 02/18] exposed addBackground function in ts --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index ab0684d..d112c13 100644 --- a/index.d.ts +++ b/index.d.ts @@ -92,6 +92,7 @@ declare module 'pdfkit-table' class PDFDocumentWithTables extends PDFDocument { public table(table: Table, options?: Options): Promise; + public addBackground(rect: Rect, fillColor: string, fillOpacity: number); } // export = PDFDocumentWithTables; From 7c65b314d8b881428a72e19e4279197310c02992 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 12:26:45 +0300 Subject: [PATCH 03/18] added render queue onAddPage --- index.d.ts | 1 + index.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/index.d.ts b/index.d.ts index d112c13..1876d8d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -92,6 +92,7 @@ declare module 'pdfkit-table' class PDFDocumentWithTables extends PDFDocument { public table(table: Table, options?: Options): Promise; + public queueRenderOnAddPage(section: (doc: PDFDocumentWithTables) => void); public addBackground(rect: Rect, fillColor: string, fillOpacity: number); } diff --git a/index.js b/index.js index 7805b0c..a1818a1 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const PDFDocument = require("pdfkit"); // const EventEmitter = require('events').EventEmitter; class PDFDocumentWithTables extends PDFDocument { + rendersOnAddPage = [] constructor(option) { super(option); @@ -13,6 +14,20 @@ class PDFDocumentWithTables extends PDFDocument { // this.emitter = new EventEmitter(); } + /** + * queueRenderOnAddPage + * @param {(doc: PDFDocumentWithTables) => void} section + */ + queueRenderOnAddPage(section) { + this.rendersOnAddPage.push(section) + } + + addPage(){ + this.rendersOnAddPage.forEach(section => section(this)) + super.addPage() + } + + logg(...args) { // console.log(args); } From 799b4db01511af7b1a60f550c2f707cd77f54ff6 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 12:32:17 +0300 Subject: [PATCH 04/18] setting renders queue in constructor --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index a1818a1..603dee9 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ class PDFDocumentWithTables extends PDFDocument { constructor(option) { super(option); this.opt = option; + this.rendersOnAddPage = [] // this.emitter = new EventEmitter(); } From 7aa898f513c5d7016e7d6a1f529a93ed6c5c6af3 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 12:42:24 +0300 Subject: [PATCH 05/18] fixed override for addPage --- index.d.ts | 2 +- index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1876d8d..d43cc71 100644 --- a/index.d.ts +++ b/index.d.ts @@ -92,7 +92,7 @@ declare module 'pdfkit-table' class PDFDocumentWithTables extends PDFDocument { public table(table: Table, options?: Options): Promise; - public queueRenderOnAddPage(section: (doc: PDFDocumentWithTables) => void); + public queueRenderOnAddPage(section: (doc: PDFDocumentWithTables) => void) public addBackground(rect: Rect, fillColor: string, fillOpacity: number); } diff --git a/index.js b/index.js index 603dee9..d476e83 100644 --- a/index.js +++ b/index.js @@ -23,9 +23,9 @@ class PDFDocumentWithTables extends PDFDocument { this.rendersOnAddPage.push(section) } - addPage(){ + addPage(options){ this.rendersOnAddPage.forEach(section => section(this)) - super.addPage() + super.addPage(options) } From 37cf1d967989681f718d07c410c88b9cc3a2f6e3 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 12:47:22 +0300 Subject: [PATCH 06/18] added debugging logs --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index d476e83..981dbb2 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,6 @@ class PDFDocumentWithTables extends PDFDocument { constructor(option) { super(option); this.opt = option; - this.rendersOnAddPage = [] // this.emitter = new EventEmitter(); } @@ -21,9 +20,11 @@ class PDFDocumentWithTables extends PDFDocument { */ queueRenderOnAddPage(section) { this.rendersOnAddPage.push(section) + console.log(this.rendersOnAddPage) } addPage(options){ + console.log(this.rendersOnAddPage) this.rendersOnAddPage.forEach(section => section(this)) super.addPage(options) } From 8ddb56e4fd3c1ae65c6cc1e9886170973ee08766 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 13:01:06 +0300 Subject: [PATCH 07/18] logging for deb debugging --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 981dbb2..28ebcf1 100644 --- a/index.js +++ b/index.js @@ -6,11 +6,12 @@ const PDFDocument = require("pdfkit"); // const EventEmitter = require('events').EventEmitter; class PDFDocumentWithTables extends PDFDocument { - rendersOnAddPage = [] + rendersOnAddPage; constructor(option) { super(option); this.opt = option; + this.rendersOnAddPage = [] // this.emitter = new EventEmitter(); } @@ -24,6 +25,7 @@ class PDFDocumentWithTables extends PDFDocument { } addPage(options){ + console.log(this) console.log(this.rendersOnAddPage) this.rendersOnAddPage.forEach(section => section(this)) super.addPage(options) From fde7c73a7e5c8bc52096b12f73d50616547b9d65 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 13:06:00 +0300 Subject: [PATCH 08/18] removed event handler --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 28ebcf1..e9a5eee 100644 --- a/index.js +++ b/index.js @@ -922,7 +922,7 @@ class PDFDocumentWithTables extends PDFDocument { this.moveDown(); // break // add fire - this.off("pageAdded", onFirePageAdded); + // this.off("pageAdded", onFirePageAdded); // callback typeof callback === 'function' && callback(this); From 75916e764214b68caacc47a3817a23a2d6ae0bbb Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 13:30:14 +0300 Subject: [PATCH 09/18] added callback --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index e9a5eee..3348b4e 100644 --- a/index.js +++ b/index.js @@ -19,9 +19,11 @@ class PDFDocumentWithTables extends PDFDocument { * queueRenderOnAddPage * @param {(doc: PDFDocumentWithTables) => void} section */ - queueRenderOnAddPage(section) { + queueRenderOnAddPage(section, callback) { this.rendersOnAddPage.push(section) + console.log(this.rendersOnAddPage) + typeof callback === 'function' && callback(this); } addPage(options){ From e5d34e68fc9eb1292029650c2137e3b2db6d27f8 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 13:41:51 +0300 Subject: [PATCH 10/18] rendering set in onFirePageAdded --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 3348b4e..d3fac48 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,6 @@ class PDFDocumentWithTables extends PDFDocument { addPage(options){ console.log(this) console.log(this.rendersOnAddPage) - this.rendersOnAddPage.forEach(section => section(this)) super.addPage(options) } @@ -217,6 +216,7 @@ class PDFDocumentWithTables extends PDFDocument { size: this.page.size, margins: this.page.margins, }); + this.rendersOnAddPage.forEach(section => section(this)) lockAddHeader || addHeader(); //addHeader(); }; From 14851005136953ad3fdee61fbb49ba36ea0ed1b3 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 13:52:55 +0300 Subject: [PATCH 11/18] removed logs and disabled event triggers --- index.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/index.js b/index.js index d3fac48..0a79404 100644 --- a/index.js +++ b/index.js @@ -21,17 +21,9 @@ class PDFDocumentWithTables extends PDFDocument { */ queueRenderOnAddPage(section, callback) { this.rendersOnAddPage.push(section) - - console.log(this.rendersOnAddPage) typeof callback === 'function' && callback(this); } - addPage(options){ - console.log(this) - console.log(this.rendersOnAddPage) - super.addPage(options) - } - logg(...args) { // console.log(args); @@ -924,7 +916,7 @@ class PDFDocumentWithTables extends PDFDocument { this.moveDown(); // break // add fire - // this.off("pageAdded", onFirePageAdded); + this.off("pageAdded", onFirePageAdded); // callback typeof callback === 'function' && callback(this); From dd3fdf8ddab3587310accec9dac9ea379a20da42 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 14:09:52 +0300 Subject: [PATCH 12/18] logging for debugging --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 0a79404..a781105 100644 --- a/index.js +++ b/index.js @@ -670,6 +670,7 @@ class PDFDocumentWithTables extends PDFDocument { // For safety, consider 3 rows margin instead of just one // if (startY + 2 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows // else this.emitter.emit('addPage'); //this.addPage(); + console.log('plugin datas', this.y, this.y + safelyMarginBottom + rowHeight) if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); // calc position From 809b430c2a86ed6a08bb7be450c3053be759632e Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 15:29:49 +0300 Subject: [PATCH 13/18] testing sections order --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a781105..19a3cef 100644 --- a/index.js +++ b/index.js @@ -203,12 +203,12 @@ class PDFDocumentWithTables extends PDFDocument { startY = this.page.margins.top; rowBottomY = 0; // lockAddPage || this.addPage(this.options); + this.rendersOnAddPage.forEach(section => section(this)) lockAddPage || this.addPage({ layout: this.page.layout, size: this.page.size, margins: this.page.margins, }); - this.rendersOnAddPage.forEach(section => section(this)) lockAddHeader || addHeader(); //addHeader(); }; From 978bbb2871083a9c333744bb620cc1506e673730 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 15:40:11 +0300 Subject: [PATCH 14/18] refactored code --- index.js | 1091 +++++++---------------------------------------- legacy-index.js | 981 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1144 insertions(+), 928 deletions(-) create mode 100644 legacy-index.js diff --git a/index.js b/index.js index 19a3cef..67b8f45 100644 --- a/index.js +++ b/index.js @@ -1,981 +1,216 @@ -// jshint esversion: 6 -// "use strict"; -// https://jshint.com/ - const PDFDocument = require("pdfkit"); -// const EventEmitter = require('events').EventEmitter; class PDFDocumentWithTables extends PDFDocument { - rendersOnAddPage; - - constructor(option) { - super(option); - this.opt = option; - this.rendersOnAddPage = [] - // this.emitter = new EventEmitter(); - } + renderCallbacksOnNewPage; - /** - * queueRenderOnAddPage - * @param {(doc: PDFDocumentWithTables) => void} section - */ - queueRenderOnAddPage(section, callback) { - this.rendersOnAddPage.push(section) - typeof callback === 'function' && callback(this); + constructor(options) { + super(options); + this.options = options; + this.renderCallbacksOnNewPage = []; } - - logg(...args) { - // console.log(args); + queueRenderOnNewPage(renderFn, callback) { + this.renderCallbacksOnNewPage.push(renderFn); + if (typeof callback === "function") callback(this); } - /** - * addBackground - * @param {Object} rect - * @param {String} fillColor - * @param {Number} fillOpacity - * @param {Function} callback - */ - addBackground ({x, y, width, height}, fillColor, fillOpacity, callback) { - - // validate - fillColor || (fillColor = 'grey'); - fillOpacity || (fillOpacity = 0.1); - - // save current style - this.save(); - - // draw bg - this - .fill(fillColor) - //.stroke(fillColor) - .fillOpacity(fillOpacity) - .rect( x, y, width, height ) - //.stroke() - .fill(); + addRectBackground({ x, y, width, height }, fillColor = "grey", fillOpacity = 0.1, callback) { + this.save() + .fill(fillColor) + .fillOpacity(fillOpacity) + .rect(x, y, width, height) + .fill() + .restore(); - // back to saved style - this.restore(); - - // restore - // this - // .fillColor('black') - // .fillOpacity(1) - // .fill(); - - typeof callback === 'function' && callback(this); - + if (typeof callback === "function") callback(this); } - /** - * table - * @param {Object} table - * @param {Object} options - * @param {Function} callback - */ - table(table, options, callback) { + async renderTable(tableData, userOptions, callback) { return new Promise((resolve, reject) => { try { - - typeof table === 'string' && (table = JSON.parse(table)); - - table || (table = {}); - options || (options = {}); - - table.headers || (table.headers = []); - table.datas || (table.datas = []); - table.rows || (table.rows = []); - table.options && (options = {...options, ...table.options}); - - options.hideHeader || (options.hideHeader = false); - options.padding || (options.padding = 0); - options.columnsSize || (options.columnsSize = []); - options.addPage || (options.addPage = false); - options.absolutePosition || (options.absolutePosition = false); - options.minRowHeight || (options.minRowHeight = 0); - // TODO options.hyperlink || (options.hyperlink = { urlToLink: false, description: null }); - - // divider lines - options.divider || (options.divider = {}); - options.divider.header || (options.divider.header = { disabled: false, width: undefined, opacity: undefined }); - options.divider.horizontal || (options.divider.horizontal = { disabled: false, width: undefined, opacity: undefined }); - options.divider.vertical || (options.divider.vertical = { disabled: true, width: undefined, opacity: undefined }); - - if(!table.headers.length) throw new Error('Headers not defined. Use options: hideHeader to hide.'); - - if(options.useSafelyMarginBottom === undefined) options.useSafelyMarginBottom = true; - - const title = table.title ? table.title : ( options.title || '' ) ; - const subtitle = table.subtitle ? table.subtitle : ( options.subtitle || '' ) ; - - this.logg('layout', this.page.layout); - this.logg('size', this.page.size); - this.logg('margins', this.page.margins); - // this.logg('options', this.options); - - // const columnIsDefined = options.columnsSize.length ? true : false; - const columnSpacing = options.columnSpacing || 3; // 15 - let columnSizes = []; - let columnPositions = []; // 0, 10, 20, 30, 100 - let columnWidth = 0; - - const rowDistance = 0.5; - let cellPadding = {top: 0, right: 0, bottom: 0, left: 0}; // universal - - const prepareHeader = options.prepareHeader || (() => this.fillColor('black').font("Helvetica-Bold").fontSize(8).fill()); - const prepareRow = options.prepareRow || ((row, indexColumn, indexRow, rectRow, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); - //const prepareCell = options.prepareCell || ((cell, indexColumn, indexRow, indexCell, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); - - let tableWidth = 0; - const maxY = this.page.height - (this.page.margins.bottom); // this.page.margins.top + - - let startX = options.x || this.x || this.page.margins.left; - let startY = options.y || this.y || this.page.margins.top; - - let lastPositionX = 0; - let rowBottomY = 0; - - //------------ experimental fast variables - let titleHeight = 0; - this.headerHeight = 0; - let firstLineHeight = 0; - this.datasIndex = 0; - this.rowsIndex = 0 ; - let lockAddTitles = false; // to addd title one time - let lockAddPage = false; - let lockAddHeader = false; - let safelyMarginBottom = this.page.margins.top/2; - - // reset position to margins.left - if( options.x === null || options.x === -1 ){ - startX = this.page.margins.left; - } - - const createTitle = ( data, size, opacity ) => { - - // Title - if(!data) return; - - // get height line - // let cellHeight = 0; - // if string - if(typeof data === 'string' ){ - // font size - this.fillColor('black').fontSize(8).fontSize(size).opacity(opacity).fill(); - // this.fillColor('black').font("Helvetica").fontSize(8).fontSize(size).opacity(opacity).fill(); - - // const titleHeight = this.heightOfString(data, { - // width: tableWidth, - // align: 'left', - // }); - this.logg(data, titleHeight); // 24 - - // write - this.text( data, startX, startY ).opacity( 1 ); // moveDown( 0.5 ) - // startY += cellHeight; - startY = this.y + columnSpacing + 2; - // else object - } else if(typeof data === 'object' ){ - // title object - data.fontFamily && this.font( data.fontFamily ); - data.label && this.fillColor( data.color || 'black').fontSize( data.fontSize || size ).text( data.label, startX, startY ).fill(); - - startY = this.y + columnSpacing + 2; - - } + if (typeof tableData === "string") tableData = JSON.parse(tableData); + if (!tableData) tableData = {}; + if (!userOptions) userOptions = {}; + + tableData.headers ??= []; + tableData.datas ??= []; + tableData.rows ??= []; + if (tableData.options) userOptions = { ...userOptions, ...tableData.options }; + + const defaults = { + hideHeader: false, + padding: 0, + columnsSize: [], + addPage: false, + absolutePosition: false, + minRowHeight: 0, + divider: { + header: { disabled: false }, + horizontal: { disabled: false }, + vertical: { disabled: true }, + }, }; - - // add a new page before crate table - options.addPage === true && onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // // create title and subtitle - // createTitle( title, 12, 1 ); - // createTitle( subtitle, 9, 0.7 ); - - // add space after title - // if( title || subtitle ){ - // startY += 3; - // }; - - // event emitter - const onFirePageAdded = () => { - // startX = this.page.margins.left; + const options = { ...defaults, ...userOptions }; + + if (!tableData.headers.length && !options.hideHeader) + throw new Error("Headers not defined. Use hideHeader option to skip."); + + const title = tableData.title || options.title || ""; + const subtitle = tableData.subtitle || options.subtitle || ""; + const columnSpacing = options.columnSpacing || 3; + let columnSizes = []; + let columnPositions = []; + let tableWidth = 0; + let lastX = 0; + let startX = options.x ?? this.page.margins.left; + let startY = options.y ?? this.page.margins.top; + const maxY = this.page.height - this.page.margins.bottom; + const safeBottomMargin = this.page.margins.top / 2; + + const prepareHeader = options.prepareHeader || (() => this.fillColor("black").font("Helvetica-Bold").fontSize(8)); + const prepareRow = options.prepareRow || (() => this.fillColor("black").font("Helvetica").fontSize(8)); + + const onNewPage = () => { startY = this.page.margins.top; - rowBottomY = 0; - // lockAddPage || this.addPage(this.options); - this.rendersOnAddPage.forEach(section => section(this)) - lockAddPage || this.addPage({ + this.addPage({ layout: this.page.layout, size: this.page.size, margins: this.page.margins, }); - lockAddHeader || addHeader(); - //addHeader(); + this.renderCallbacksOnNewPage.forEach((fn) => fn(this)); + addTableHeader(); }; - - // add fire - // this.emitter.removeAllListeners(); - // this.emitter.on('addTitle', addTitle); - // this.emitter.on('addSubtitle', addSubTitle); - // this.emitter.on('addPage', onFirePageAdded); - // this.emitter.emit('addPage'); - // this.on('pageAdded', onFirePageAdded); - // warning - eval can be harmful - const fEval = (str) => { - let f = null; eval('f = ' + str); return f; + const evalFunction = (code) => { + let fn = null; + eval("fn = " + code); + return fn; }; - - const separationsColumn = () => { - // soon - } - - const separationsRow = (type, x, y, width, opacity, color) => { - type || (type = 'horizontal'); // header | horizontal | vertical - - // distance - const d = rowDistance * 1.5; - // margin - const m = options.x || this.page.margins.left || 30; - // disabled - const s = options.divider[type].disabled || false; - - if(s === true) return; - opacity = opacity || options.divider[type].opacity || 0.5; - width = width || options.divider[type].width || 0.5; - color = color || options.divider[type].color || 'black'; - - // draw - this - .moveTo(x, y - d) - .lineTo(x + tableWidth - m, y - d) - .lineWidth(width) - .strokeColor(color) - .opacity(opacity) - .stroke() - // Reset opacity after drawing the line - .opacity(1); - - }; - - // padding: [10, 10, 10, 10] - // padding: [10, 10] - // padding: {top: 10, right: 10, bottom: 10, left: 10} - // padding: 10, - const prepareCellPadding = (p) => { - - // array - if(Array.isArray(p)){ - switch(p.length){ - case 3: p = [...p, 0]; break; - case 2: p = [...p, ...p]; break; - case 1: p = Array(4).fill(p[0]); break; - } - } - // number - else if(typeof p === 'number'){ - p = Array(4).fill(p); - } - // object - else if(typeof p === 'object'){ - const {top, right, bottom, left} = p; - p = [top, right, bottom, left]; - } - // null - else { - p = Array(4).fill(0); - } - - return { - top: p[0] >> 0, // int - right: p[1] >> 0, - bottom: p[2] >> 0, - left: p[3] >> 0, - }; - - }; - - const prepareRowOptions = (row) => { - - // validate - if( typeof row !== 'object' || !row.hasOwnProperty('options') ) return; - - const {fontFamily, fontSize, color} = row.options; - - fontFamily && this.font(fontFamily); - fontSize && this.fontSize(fontSize); - color && this.fillColor(color); - - // row.options.hasOwnProperty('fontFamily') && this.font(row.options.fontFamily); - // row.options.hasOwnProperty('fontSize') && this.fontSize(row.options.fontSize); - // row.options.hasOwnProperty('color') && this.fillColor(row.options.color); - - }; - - const prepareRowBackground = (row, rect) => { - - // validate - if(typeof row !== 'object') return; - - // options - row.options && (row = row.options); - - let { fill, opac } = {}; - - // add backgroundColor - if(row.hasOwnProperty('columnColor')){ // ^0.1.70 - - const { columnColor, columnOpacity } = row; - fill = columnColor; - opac = columnOpacity; - - } else if(row.hasOwnProperty('backgroundColor')){ // ~0.1.65 old - - const { backgroundColor, backgroundOpacity } = row; - fill = backgroundColor; - opac = backgroundOpacity; - - } else if(row.hasOwnProperty('background')){ // dont remove - - if(typeof row.background === 'object'){ - let { color, opacity } = row.background; - fill = color; - opac = opacity; - } - + const parsePadding = (padding) => { + if (Array.isArray(padding)) { + if (padding.length === 2) padding = [...padding, ...padding]; + else if (padding.length === 1) padding = Array(4).fill(padding[0]); + } else if (typeof padding === "number") { + padding = Array(4).fill(padding); + } else if (typeof padding === "object") { + const { top, right, bottom, left } = padding; + padding = [top, right, bottom, left]; + } else { + padding = [0, 0, 0, 0]; } - - fill && this.addBackground(rect, fill, opac); - + return { top: padding[0], right: padding[1], bottom: padding[2], left: padding[3] }; }; - - const computeRowHeight = (row, isHeader) => { - - let result = isHeader ? 0 : (options.minRowHeight || 0); - let cellp; - - // if row is object, content with property and options - if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ - const cells = []; - // get all properties names on header - table.headers.forEach(({property}) => cells.push(row[property]) ); - // define row with properties header - row = cells; - } - - row.forEach((cell,i) => { - - let text = cell; - - // object - // read cell and get label of object - if( typeof cell === 'object' ){ - // define label - text = String(cell.label); - // apply font size on calc about height row - cell.hasOwnProperty('options') && prepareRowOptions(cell); - } - - text = String(text).replace('bold:','').replace('size',''); - - // cell padding - cellp = prepareCellPadding(table.headers[i].padding || options.padding || 0); - // cellp = prepareCellPadding(options.padding || 0); - // - (cellp.left + cellp.right + (columnSpacing * 2)) - // console.log(cellp); - - // calc height size of string - const cellHeight = this.heightOfString(text, { - width: columnSizes[i] - (cellp.left + cellp.right), - align: 'left', - }); - - result = Math.max(result, cellHeight); - }); - // isHeader && (result = Math.max(result, options.minRowHeight)); + const drawDivider = (type, x, y, width, opacity, color) => { + const divider = options.divider[type] ?? {}; + if (divider.disabled) return; + + const strokeOpacity = opacity ?? divider.opacity ?? 0.5; + const strokeWidth = width ?? divider.width ?? 0.5; + const strokeColor = color ?? divider.color ?? "black"; + + this.moveTo(x, y - 0.75) + .lineTo(x + tableWidth - startX, y - 0.75) + .lineWidth(strokeWidth) + .strokeColor(strokeColor) + .opacity(strokeOpacity) + .stroke() + .opacity(1); + }; - // if(result + columnSpacing === 0) { - // computeRowHeight(row); - // } - - return result + (columnSpacing); + const setRowStyles = (row) => { + if (typeof row !== "object" || !row.options) return; + const { fontFamily, fontSize, color } = row.options; + if (fontFamily) this.font(fontFamily); + if (fontSize) this.fontSize(fontSize); + if (color) this.fillColor(color); }; - - // Calc columns size - - const calcColumnSizes = () => { - - let h = []; // header width - let p = []; // position - let w = 0; // table width - - // (table width) 1o - Max size table - w = this.page.width - this.page.margins.right - ( options.x || this.page.margins.left ); - // (table width) 2o - Size defined - options.width && ( w = parseInt(options.width) || String(options.width).replace(/[^0-9]/g,'') >> 0 ); - - // (table width) if table is percent of page - // ... - - // (size columns) 1o - table.headers.forEach( el => { - el.width && h.push(el.width); // - columnSpacing - }); - // (size columns) 2o - if(h.length === 0) { - h = options.columnsSize; - } - // (size columns) 3o - if(h.length === 0) { - columnWidth = ( w / table.headers.length ); // - columnSpacing // define column width - table.headers.forEach( () => h.push(columnWidth) ); - } - - // Set columnPositions - h.reduce((prev, curr, indx) => { - p.push(prev >> 0); - return prev + curr; - },( options.x || this.page.margins.left )); - - // !Set columnSizes - h.length && (columnSizes = h); - p.length && (columnPositions = p); - - // (table width) 3o - Sum last position + lest header width - w = p[p.length-1] + h[h.length-1]; - - // !Set tableWidth - w && ( tableWidth = w ); - - // Ajust spacing - // tableWidth = tableWidth - (h.length * columnSpacing); - - this.logg('columnSizes', h); - this.logg('columnPositions', p); - + + const drawRowBackground = (row, rect) => { + if (typeof row !== "object") return; + const opts = row.options ?? row; + const color = opts.columnColor ?? opts.backgroundColor ?? opts.background?.color; + const opacity = opts.columnOpacity ?? opts.backgroundOpacity ?? opts.background?.opacity; + if (color) this.addRectBackground(rect, color, opacity); }; - - calcColumnSizes(); - - // Header - - const addHeader = () => { - - // Allow the user to override style for headers - prepareHeader(); - - // calc header height - if(this.headerHeight === 0){ - this.headerHeight = computeRowHeight(table.headers, true); - this.logg(this.headerHeight, 'headers'); - } - // calc first table line when init table - if(firstLineHeight === 0){ - if(table.datas.length > 0){ - firstLineHeight = computeRowHeight(table.datas[0], true); - this.logg(firstLineHeight, 'datas'); - } - if(table.rows.length > 0){ - firstLineHeight = computeRowHeight(table.rows[0], true); - this.logg(firstLineHeight, 'rows'); + const calcColumnLayout = () => { + const headerWidths = tableData.headers.map((h) => h.width).filter(Boolean); + const tableMaxWidth = options.width + ? parseInt(options.width) || this.page.width - this.page.margins.right - startX + : this.page.width - this.page.margins.right - startX; + + if (!headerWidths.length) { + if (options.columnsSize.length) headerWidths.push(...options.columnsSize); + else { + const defaultWidth = tableMaxWidth / tableData.headers.length; + for (let i = 0; i < tableData.headers.length; i++) headerWidths.push(defaultWidth); } } - // 24.1 is height calc title + subtitle - titleHeight = !lockAddTitles ? 24.1 : 0; - // calc if header + first line fit on last page - const calc = startY + titleHeight + firstLineHeight + this.headerHeight + safelyMarginBottom// * 1.3; - - // content is big text (crazy!) - if(firstLineHeight > maxY) { - // lockAddHeader = true; - lockAddPage = true; - this.logg('CRAZY! This a big text on cell'); - } else if(calc > maxY) { // && !lockAddPage - // lockAddHeader = false; - lockAddPage = false; - onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - return; - } - - // if has title - if(lockAddTitles === false) { + columnSizes = headerWidths; + columnPositions = headerWidths.reduce((acc, width, i) => { + acc.push((acc[i - 1] ?? startX) + width); + return acc; + }, []); - // create title and subtitle - createTitle( title, 12, 1 ); - createTitle( subtitle, 9, 0.7 ); - - // add space after title - if( title || subtitle ){ - startY += 3; - }; - - } + tableWidth = columnPositions[columnPositions.length - 1]; + }; - // Allow the user to override style for headers + const addTableHeader = () => { prepareHeader(); - - lockAddTitles = true; - - // this options is trial - if(options.absolutePosition === true){ - lastPositionX = options.x || startX || this.x; // x position head - startY = options.y || startY || this.y; // x position head - } else { - lastPositionX = startX; // x position head + const headerHeight = computeRowHeight(tableData.headers, true); + const firstRowHeight = + (tableData.datas.length && computeRowHeight(tableData.datas[0], false)) || + (tableData.rows.length && computeRowHeight(tableData.rows[0], false)) || + 0; + + if (startY + headerHeight + firstRowHeight + safeBottomMargin > maxY) { + onNewPage(); + return; } - - // Check to have enough room for header and first rows. default 3 - // if (startY + 2 * this.headerHeight >= maxY) this.emitter.emit('addPage'); //this.addPage(); - - if(!options.hideHeader && table.headers.length > 0) { - - // simple header - if(typeof table.headers[0] === 'string') { - - // // background header - // const rectRow = { - // x: startX, - // y: startY - columnSpacing - (rowDistance * 2), - // width: columnWidth, - // height: this.headerHeight + columnSpacing, - // }; - - // // add background - // this.addBackground(rectRow); - - // print headers - table.headers.forEach((header, i) => { - - // background header - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: columnSizes[i], - height: this.headerHeight + columnSpacing, - }; - - // add background - this.addBackground(rectCell); - - // cell padding - cellPadding = prepareCellPadding(options.padding || 0); - - // write - this.text(header, - lastPositionX + (cellPadding.left), - startY, { - width: Number(columnSizes[i]) - (cellPadding.left + cellPadding.right), - align: 'left', - }); - - lastPositionX += columnSizes[i] >> 0; - - }); - - }else{ - - // Print all headers - table.headers.forEach( (dataHeader, i) => { - - let {label, width, renderer, align, headerColor, headerOpacity, headerAlign, padding} = dataHeader; - // check defination - width = width || columnSizes[i]; - align = headerAlign || align || 'left'; - // force number - width = width >> 0; - - // register renderer function - if(renderer && typeof renderer === 'string') { - table.headers[i].renderer = fEval(renderer); - } - - // # Rotation - // var doTransform = function (x, y, angle) { - // var rads = angle / 180 * Math.PI; - // var newX = x * Math.cos(rads) + y * Math.sin(rads); - // var newY = y * Math.cos(rads) - x * Math.sin(rads); - - // return { - // x: newX, - // y: newY, - // rads: rads, - // angle: angle - // }; - // }; - // } - // this.save(); // rotation - // this.rotate(90, {origin: [lastPositionX, startY]}); - // width = 50; - // background header - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: width, - height: this.headerHeight + columnSpacing, - }; + if (!options.hideHeader && tableData.headers.length) { + tableData.headers.forEach((header, i) => { + const rect = { x: startX + i * columnSizes[i], y: startY, width: columnSizes[i], height: headerHeight }; + this.addRectBackground(rect, "lightgray", 0.2); - // add background - this.addBackground(rectCell, headerColor, headerOpacity); - - // cell padding - cellPadding = prepareCellPadding(padding || options.padding || 0); - - // write - this.text(label, - lastPositionX + (cellPadding.left), - startY, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }) - - lastPositionX += width; - // this.restore(); // rotation - + const pad = parsePadding(options.padding); + const text = typeof header === "string" ? header : header.label; + this.text(text, rect.x + pad.left, startY, { + width: rect.width - pad.left - pad.right, + align: "left", }); - - } - - // set style - prepareRowOptions(table.headers); - - } - - if(!options.hideHeader) { - // Refresh the y coordinate of the bottom of the headers row - rowBottomY = Math.max(startY + computeRowHeight(table.headers, true), rowBottomY); - // Separation line between headers and rows - separationsRow('header', startX, rowBottomY); - } else { - rowBottomY = startY; + }); + startY += headerHeight; } - }; - - // End header - addHeader(); - - // Datas - table.datas.forEach((row, i) => { - - this.datasIndex = i; - const rowHeight = computeRowHeight(row, false); - this.logg(rowHeight); - - // Switch to next page if we cannot go any further because the space is over. - // For safety, consider 3 rows margin instead of just one - // if (startY + 2 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - // else this.emitter.emit('addPage'); //this.addPage(); - console.log('plugin datas', this.y, this.y + safelyMarginBottom + rowHeight) - if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // calc position - startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - - // unlock add page function - lockAddPage = false; - - const rectRow = { - x: startX, - y: startY - columnSpacing - (rowDistance * 2), - width: tableWidth - startX, - height: rowHeight + columnSpacing, - }; - - // add background row - prepareRowBackground(row, rectRow); - - lastPositionX = startX; - - // Print all cells of the current row - table.headers.forEach(( dataHeader, index) => { - - let {property, width, renderer, align, valign, padding} = dataHeader; - - // check defination - width = width || columnWidth; - align = align || 'left'; - - // cell padding - cellPadding = prepareCellPadding(padding || options.padding || 0); - - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: width, - height: rowHeight + columnSpacing, - } - - // allow the user to override style for rows - prepareRowOptions(row); - prepareRow(row, index, i, rectRow, rectCell,); - - let text = row[property]; - - // cell object - if(typeof text === 'object' ){ - - text = String(text.label); // get label - // row[property].hasOwnProperty('options') && prepareRowOptions(row[property]); // set style - - // options if text cell is object - if( row[property].hasOwnProperty('options') ){ - - // set font style - prepareRowOptions(row[property]); - prepareRowBackground(row[property], rectCell); - - } - - } else { - - // style column by header - prepareRowBackground(table.headers[index], rectCell); - - } - - // bold - if( String(text).indexOf('bold:') === 0 ){ - this.font('Helvetica-Bold'); - text = text.replace('bold:',''); - } - - // size - if( String(text).indexOf('size') === 0 ){ - let size = String(text).substr(4,2).replace(':','').replace('+','') >> 0; - this.fontSize( size < 7 ? 7 : size ); - text = text.replace(`size${size}:`,''); - } - - // renderer column - // renderer && (text = renderer(text, index, i, row, rectRow, rectCell)) // value, index-column, index-row, row nbhmn - if(typeof renderer === 'function'){ - text = renderer(text, index, i, row, rectRow, rectCell); // value, index-column, index-row, row, doc[this] - } - - // TODO # Experimental - // ------------------------------------------------------------------------------ - // align vertically - let topTextToAlignVertically = 0; - if(valign && valign !== 'top'){ - const heightText = this.heightOfString(text, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }); - // line height, spacing hehight, cell and text diference - topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; - } - // ------------------------------------------------------------------------------ - - this.text(text, - lastPositionX + (cellPadding.left), - startY + topTextToAlignVertically, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }); - - lastPositionX += width; - - // set style - // Maybe REMOVE ??? - prepareRowOptions(row); - prepareRow(row, index, i, rectRow, rectCell); - - }); - - // Refresh the y coordinate of the bottom of this row - rowBottomY = Math.max(startY + rowHeight, rowBottomY); - // console.log(this.page.height, rowBottomY, this.y); - // text is so big as page (crazy!) - if(rowBottomY > this.page.height) { - rowBottomY = this.y + columnSpacing + (rowDistance * 2); - } - - // Separation line between rows - separationsRow('horizontal', startX, rowBottomY); - - // review this code - if( row.hasOwnProperty('options') ){ - if( row.options.hasOwnProperty('separation') ){ - // Separation line between rows - separationsRow('horizontal',startX, rowBottomY, 1, 1); - } - } - - }); - // End datas - - // Rows - table.rows.forEach((row, i) => { - - this.rowsIndex = i; - const rowHeight = computeRowHeight(row, false); - this.logg(rowHeight); - - // Switch to next page if we cannot go any further because the space is over. - // For safety, consider 3 rows margin instead of just one - // if (startY + 3 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - // else this.emitter.emit('addPage'); //this.addPage(); - if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // calc position - startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - - // unlock add page function - lockAddPage = false; - - const rectRow = { - x: columnPositions[0], - // x: startX, - y: startY - columnSpacing - (rowDistance * 2), - width: tableWidth - startX, - height: rowHeight + columnSpacing, - } - - // add background - // doc.addBackground(rectRow); - - lastPositionX = startX; - - row.forEach((cell, index) => { - - let align = 'left'; - let valign = undefined; - - const rectCell = { - // x: columnPositions[index], - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: columnSizes[index], - height: rowHeight + columnSpacing, - } - - prepareRowBackground(table.headers[index], rectCell); - - // Allow the user to override style for rows - prepareRow(row, index, i, rectRow, rectCell); - - if(typeof table.headers[index] === 'object') { - // renderer column - table.headers[index].renderer && (cell = table.headers[index].renderer(cell, index, i, row, rectRow, rectCell, this)); // text-cell, index-column, index-line, row, doc[this] - // align - table.headers[index].align && (align = table.headers[index].align); - table.headers[index].valign && (valign = table.headers[index].valign); - } - - // cell padding - cellPadding = prepareCellPadding(table.headers[index].padding || options.padding || 0); - - // TODO # Experimental - // ------------------------------------------------------------------------------ - // align vertically - let topTextToAlignVertically = 0; - if(valign && valign !== 'top'){ - const heightText = this.heightOfString(cell, { - width: columnSizes[index] - (cellPadding.left + cellPadding.right), - align: align, - }); - // line height, spacing hehight, cell and text diference - topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; - } - // ------------------------------------------------------------------------------ - - this.text(cell, - lastPositionX + (cellPadding.left), - startY + topTextToAlignVertically, { - width: columnSizes[index] - (cellPadding.left + cellPadding.right), - align: align, + const computeRowHeight = (row, isHeader) => { + let maxHeight = isHeader ? 0 : options.minRowHeight || 0; + row.forEach((cell, i) => { + let text = typeof cell === "object" ? String(cell.label ?? "") : String(cell); + const pad = parsePadding(tableData.headers[i]?.padding ?? options.padding); + const height = this.heightOfString(text, { + width: columnSizes[i] - pad.left - pad.right, + align: "left", }); - - lastPositionX += columnSizes[index]; - + maxHeight = Math.max(maxHeight, height); }); - - // Refresh the y coordinate of the bottom of this row - rowBottomY = Math.max(startY + rowHeight, rowBottomY); - - // console.log(this.page.height, rowBottomY, this.y); - // text is so big as page (crazy!) - if(rowBottomY > this.page.height) { - rowBottomY = this.y + columnSpacing + (rowDistance * 2); - } - - // Separation line between rows - separationsRow('horizontal', startX, rowBottomY); - - }); - // End rows - - // update position - this.x = startX; - this.y = rowBottomY; // position y final; - this.moveDown(); // break - - // add fire - this.off("pageAdded", onFirePageAdded); - - // callback - typeof callback === 'function' && callback(this); - - // nice :) - resolve(); - - } catch (error) { - - // error - reject(error); - - } - - }); - } - - /** - * tables - * @param {Object} tables - * @returns - */ - async tables(tables, callback) { - return new Promise(async (resolve, reject) => { - try { - - if(Array.isArray(tables) === false) - { - resolve(); - return; - } + return maxHeight + columnSpacing; + }; - const len = tables.length; - for(let i; i < len; i++) - { - await this.table(tables[i], tables[i].options || {}); - } + calcColumnLayout(); + addTableHeader(); - // if tables is Array - // Array.isArray(tables) ? - // // for each on Array - // tables.forEach( async table => await this.table( table, table.options || {} ) ) : - // // else is tables is a unique table object - // ( typeof tables === 'object' ? this.table( tables, tables.options || {} ) : null ) ; - // // callback - typeof callback === 'function' && callback(this); - // // donw! - resolve(); - } - catch(error) - { - reject(error); + if (typeof callback === "function") callback(this); + resolve(this); + } catch (err) { + reject(err); } - }); } - } module.exports = PDFDocumentWithTables; -module.exports.default = PDFDocumentWithTables; diff --git a/legacy-index.js b/legacy-index.js new file mode 100644 index 0000000..a781105 --- /dev/null +++ b/legacy-index.js @@ -0,0 +1,981 @@ +// jshint esversion: 6 +// "use strict"; +// https://jshint.com/ + +const PDFDocument = require("pdfkit"); +// const EventEmitter = require('events').EventEmitter; + +class PDFDocumentWithTables extends PDFDocument { + rendersOnAddPage; + + constructor(option) { + super(option); + this.opt = option; + this.rendersOnAddPage = [] + // this.emitter = new EventEmitter(); + } + + /** + * queueRenderOnAddPage + * @param {(doc: PDFDocumentWithTables) => void} section + */ + queueRenderOnAddPage(section, callback) { + this.rendersOnAddPage.push(section) + typeof callback === 'function' && callback(this); + } + + + logg(...args) { + // console.log(args); + } + + /** + * addBackground + * @param {Object} rect + * @param {String} fillColor + * @param {Number} fillOpacity + * @param {Function} callback + */ + addBackground ({x, y, width, height}, fillColor, fillOpacity, callback) { + + // validate + fillColor || (fillColor = 'grey'); + fillOpacity || (fillOpacity = 0.1); + + // save current style + this.save(); + + // draw bg + this + .fill(fillColor) + //.stroke(fillColor) + .fillOpacity(fillOpacity) + .rect( x, y, width, height ) + //.stroke() + .fill(); + + // back to saved style + this.restore(); + + // restore + // this + // .fillColor('black') + // .fillOpacity(1) + // .fill(); + + typeof callback === 'function' && callback(this); + + } + + /** + * table + * @param {Object} table + * @param {Object} options + * @param {Function} callback + */ + table(table, options, callback) { + return new Promise((resolve, reject) => { + try { + + typeof table === 'string' && (table = JSON.parse(table)); + + table || (table = {}); + options || (options = {}); + + table.headers || (table.headers = []); + table.datas || (table.datas = []); + table.rows || (table.rows = []); + table.options && (options = {...options, ...table.options}); + + options.hideHeader || (options.hideHeader = false); + options.padding || (options.padding = 0); + options.columnsSize || (options.columnsSize = []); + options.addPage || (options.addPage = false); + options.absolutePosition || (options.absolutePosition = false); + options.minRowHeight || (options.minRowHeight = 0); + // TODO options.hyperlink || (options.hyperlink = { urlToLink: false, description: null }); + + // divider lines + options.divider || (options.divider = {}); + options.divider.header || (options.divider.header = { disabled: false, width: undefined, opacity: undefined }); + options.divider.horizontal || (options.divider.horizontal = { disabled: false, width: undefined, opacity: undefined }); + options.divider.vertical || (options.divider.vertical = { disabled: true, width: undefined, opacity: undefined }); + + if(!table.headers.length) throw new Error('Headers not defined. Use options: hideHeader to hide.'); + + if(options.useSafelyMarginBottom === undefined) options.useSafelyMarginBottom = true; + + const title = table.title ? table.title : ( options.title || '' ) ; + const subtitle = table.subtitle ? table.subtitle : ( options.subtitle || '' ) ; + + this.logg('layout', this.page.layout); + this.logg('size', this.page.size); + this.logg('margins', this.page.margins); + // this.logg('options', this.options); + + // const columnIsDefined = options.columnsSize.length ? true : false; + const columnSpacing = options.columnSpacing || 3; // 15 + let columnSizes = []; + let columnPositions = []; // 0, 10, 20, 30, 100 + let columnWidth = 0; + + const rowDistance = 0.5; + let cellPadding = {top: 0, right: 0, bottom: 0, left: 0}; // universal + + const prepareHeader = options.prepareHeader || (() => this.fillColor('black').font("Helvetica-Bold").fontSize(8).fill()); + const prepareRow = options.prepareRow || ((row, indexColumn, indexRow, rectRow, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); + //const prepareCell = options.prepareCell || ((cell, indexColumn, indexRow, indexCell, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); + + let tableWidth = 0; + const maxY = this.page.height - (this.page.margins.bottom); // this.page.margins.top + + + let startX = options.x || this.x || this.page.margins.left; + let startY = options.y || this.y || this.page.margins.top; + + let lastPositionX = 0; + let rowBottomY = 0; + + //------------ experimental fast variables + let titleHeight = 0; + this.headerHeight = 0; + let firstLineHeight = 0; + this.datasIndex = 0; + this.rowsIndex = 0 ; + let lockAddTitles = false; // to addd title one time + let lockAddPage = false; + let lockAddHeader = false; + let safelyMarginBottom = this.page.margins.top/2; + + // reset position to margins.left + if( options.x === null || options.x === -1 ){ + startX = this.page.margins.left; + } + + const createTitle = ( data, size, opacity ) => { + + // Title + if(!data) return; + + // get height line + // let cellHeight = 0; + // if string + if(typeof data === 'string' ){ + // font size + this.fillColor('black').fontSize(8).fontSize(size).opacity(opacity).fill(); + // this.fillColor('black').font("Helvetica").fontSize(8).fontSize(size).opacity(opacity).fill(); + + // const titleHeight = this.heightOfString(data, { + // width: tableWidth, + // align: 'left', + // }); + this.logg(data, titleHeight); // 24 + + // write + this.text( data, startX, startY ).opacity( 1 ); // moveDown( 0.5 ) + // startY += cellHeight; + startY = this.y + columnSpacing + 2; + // else object + } else if(typeof data === 'object' ){ + // title object + data.fontFamily && this.font( data.fontFamily ); + data.label && this.fillColor( data.color || 'black').fontSize( data.fontSize || size ).text( data.label, startX, startY ).fill(); + + startY = this.y + columnSpacing + 2; + + } + }; + + // add a new page before crate table + options.addPage === true && onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // // create title and subtitle + // createTitle( title, 12, 1 ); + // createTitle( subtitle, 9, 0.7 ); + + // add space after title + // if( title || subtitle ){ + // startY += 3; + // }; + + // event emitter + const onFirePageAdded = () => { + // startX = this.page.margins.left; + startY = this.page.margins.top; + rowBottomY = 0; + // lockAddPage || this.addPage(this.options); + lockAddPage || this.addPage({ + layout: this.page.layout, + size: this.page.size, + margins: this.page.margins, + }); + this.rendersOnAddPage.forEach(section => section(this)) + lockAddHeader || addHeader(); + //addHeader(); + }; + + // add fire + // this.emitter.removeAllListeners(); + // this.emitter.on('addTitle', addTitle); + // this.emitter.on('addSubtitle', addSubTitle); + // this.emitter.on('addPage', onFirePageAdded); + // this.emitter.emit('addPage'); + // this.on('pageAdded', onFirePageAdded); + + // warning - eval can be harmful + const fEval = (str) => { + let f = null; eval('f = ' + str); return f; + }; + + const separationsColumn = () => { + // soon + } + + const separationsRow = (type, x, y, width, opacity, color) => { + + type || (type = 'horizontal'); // header | horizontal | vertical + + // distance + const d = rowDistance * 1.5; + // margin + const m = options.x || this.page.margins.left || 30; + // disabled + const s = options.divider[type].disabled || false; + + if(s === true) return; + opacity = opacity || options.divider[type].opacity || 0.5; + width = width || options.divider[type].width || 0.5; + color = color || options.divider[type].color || 'black'; + + // draw + this + .moveTo(x, y - d) + .lineTo(x + tableWidth - m, y - d) + .lineWidth(width) + .strokeColor(color) + .opacity(opacity) + .stroke() + // Reset opacity after drawing the line + .opacity(1); + + }; + + // padding: [10, 10, 10, 10] + // padding: [10, 10] + // padding: {top: 10, right: 10, bottom: 10, left: 10} + // padding: 10, + const prepareCellPadding = (p) => { + + // array + if(Array.isArray(p)){ + switch(p.length){ + case 3: p = [...p, 0]; break; + case 2: p = [...p, ...p]; break; + case 1: p = Array(4).fill(p[0]); break; + } + } + // number + else if(typeof p === 'number'){ + p = Array(4).fill(p); + } + // object + else if(typeof p === 'object'){ + const {top, right, bottom, left} = p; + p = [top, right, bottom, left]; + } + // null + else { + p = Array(4).fill(0); + } + + return { + top: p[0] >> 0, // int + right: p[1] >> 0, + bottom: p[2] >> 0, + left: p[3] >> 0, + }; + + }; + + const prepareRowOptions = (row) => { + + // validate + if( typeof row !== 'object' || !row.hasOwnProperty('options') ) return; + + const {fontFamily, fontSize, color} = row.options; + + fontFamily && this.font(fontFamily); + fontSize && this.fontSize(fontSize); + color && this.fillColor(color); + + // row.options.hasOwnProperty('fontFamily') && this.font(row.options.fontFamily); + // row.options.hasOwnProperty('fontSize') && this.fontSize(row.options.fontSize); + // row.options.hasOwnProperty('color') && this.fillColor(row.options.color); + + }; + + const prepareRowBackground = (row, rect) => { + + // validate + if(typeof row !== 'object') return; + + // options + row.options && (row = row.options); + + let { fill, opac } = {}; + + // add backgroundColor + if(row.hasOwnProperty('columnColor')){ // ^0.1.70 + + const { columnColor, columnOpacity } = row; + fill = columnColor; + opac = columnOpacity; + + } else if(row.hasOwnProperty('backgroundColor')){ // ~0.1.65 old + + const { backgroundColor, backgroundOpacity } = row; + fill = backgroundColor; + opac = backgroundOpacity; + + } else if(row.hasOwnProperty('background')){ // dont remove + + if(typeof row.background === 'object'){ + let { color, opacity } = row.background; + fill = color; + opac = opacity; + } + + } + + fill && this.addBackground(rect, fill, opac); + + }; + + const computeRowHeight = (row, isHeader) => { + + let result = isHeader ? 0 : (options.minRowHeight || 0); + let cellp; + + // if row is object, content with property and options + if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ + const cells = []; + // get all properties names on header + table.headers.forEach(({property}) => cells.push(row[property]) ); + // define row with properties header + row = cells; + } + + row.forEach((cell,i) => { + + let text = cell; + + // object + // read cell and get label of object + if( typeof cell === 'object' ){ + // define label + text = String(cell.label); + // apply font size on calc about height row + cell.hasOwnProperty('options') && prepareRowOptions(cell); + } + + text = String(text).replace('bold:','').replace('size',''); + + // cell padding + cellp = prepareCellPadding(table.headers[i].padding || options.padding || 0); + // cellp = prepareCellPadding(options.padding || 0); + // - (cellp.left + cellp.right + (columnSpacing * 2)) + // console.log(cellp); + + // calc height size of string + const cellHeight = this.heightOfString(text, { + width: columnSizes[i] - (cellp.left + cellp.right), + align: 'left', + }); + + result = Math.max(result, cellHeight); + }); + + // isHeader && (result = Math.max(result, options.minRowHeight)); + + // if(result + columnSpacing === 0) { + // computeRowHeight(row); + // } + + return result + (columnSpacing); + }; + + // Calc columns size + + const calcColumnSizes = () => { + + let h = []; // header width + let p = []; // position + let w = 0; // table width + + // (table width) 1o - Max size table + w = this.page.width - this.page.margins.right - ( options.x || this.page.margins.left ); + // (table width) 2o - Size defined + options.width && ( w = parseInt(options.width) || String(options.width).replace(/[^0-9]/g,'') >> 0 ); + + // (table width) if table is percent of page + // ... + + // (size columns) 1o + table.headers.forEach( el => { + el.width && h.push(el.width); // - columnSpacing + }); + // (size columns) 2o + if(h.length === 0) { + h = options.columnsSize; + } + // (size columns) 3o + if(h.length === 0) { + columnWidth = ( w / table.headers.length ); // - columnSpacing // define column width + table.headers.forEach( () => h.push(columnWidth) ); + } + + // Set columnPositions + h.reduce((prev, curr, indx) => { + p.push(prev >> 0); + return prev + curr; + },( options.x || this.page.margins.left )); + + // !Set columnSizes + h.length && (columnSizes = h); + p.length && (columnPositions = p); + + // (table width) 3o - Sum last position + lest header width + w = p[p.length-1] + h[h.length-1]; + + // !Set tableWidth + w && ( tableWidth = w ); + + // Ajust spacing + // tableWidth = tableWidth - (h.length * columnSpacing); + + this.logg('columnSizes', h); + this.logg('columnPositions', p); + + }; + + calcColumnSizes(); + + // Header + + const addHeader = () => { + + // Allow the user to override style for headers + prepareHeader(); + + // calc header height + if(this.headerHeight === 0){ + this.headerHeight = computeRowHeight(table.headers, true); + this.logg(this.headerHeight, 'headers'); + } + + // calc first table line when init table + if(firstLineHeight === 0){ + if(table.datas.length > 0){ + firstLineHeight = computeRowHeight(table.datas[0], true); + this.logg(firstLineHeight, 'datas'); + } + if(table.rows.length > 0){ + firstLineHeight = computeRowHeight(table.rows[0], true); + this.logg(firstLineHeight, 'rows'); + } + } + + // 24.1 is height calc title + subtitle + titleHeight = !lockAddTitles ? 24.1 : 0; + // calc if header + first line fit on last page + const calc = startY + titleHeight + firstLineHeight + this.headerHeight + safelyMarginBottom// * 1.3; + + // content is big text (crazy!) + if(firstLineHeight > maxY) { + // lockAddHeader = true; + lockAddPage = true; + this.logg('CRAZY! This a big text on cell'); + } else if(calc > maxY) { // && !lockAddPage + // lockAddHeader = false; + lockAddPage = false; + onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + return; + } + + // if has title + if(lockAddTitles === false) { + + // create title and subtitle + createTitle( title, 12, 1 ); + createTitle( subtitle, 9, 0.7 ); + + // add space after title + if( title || subtitle ){ + startY += 3; + }; + + } + + // Allow the user to override style for headers + prepareHeader(); + + lockAddTitles = true; + + // this options is trial + if(options.absolutePosition === true){ + lastPositionX = options.x || startX || this.x; // x position head + startY = options.y || startY || this.y; // x position head + } else { + lastPositionX = startX; // x position head + } + + // Check to have enough room for header and first rows. default 3 + // if (startY + 2 * this.headerHeight >= maxY) this.emitter.emit('addPage'); //this.addPage(); + + if(!options.hideHeader && table.headers.length > 0) { + + // simple header + if(typeof table.headers[0] === 'string') { + + // // background header + // const rectRow = { + // x: startX, + // y: startY - columnSpacing - (rowDistance * 2), + // width: columnWidth, + // height: this.headerHeight + columnSpacing, + // }; + + // // add background + // this.addBackground(rectRow); + + // print headers + table.headers.forEach((header, i) => { + + // background header + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: columnSizes[i], + height: this.headerHeight + columnSpacing, + }; + + // add background + this.addBackground(rectCell); + + // cell padding + cellPadding = prepareCellPadding(options.padding || 0); + + // write + this.text(header, + lastPositionX + (cellPadding.left), + startY, { + width: Number(columnSizes[i]) - (cellPadding.left + cellPadding.right), + align: 'left', + }); + + lastPositionX += columnSizes[i] >> 0; + + }); + + }else{ + + // Print all headers + table.headers.forEach( (dataHeader, i) => { + + let {label, width, renderer, align, headerColor, headerOpacity, headerAlign, padding} = dataHeader; + // check defination + width = width || columnSizes[i]; + align = headerAlign || align || 'left'; + // force number + width = width >> 0; + + // register renderer function + if(renderer && typeof renderer === 'string') { + table.headers[i].renderer = fEval(renderer); + } + + // # Rotation + // var doTransform = function (x, y, angle) { + // var rads = angle / 180 * Math.PI; + // var newX = x * Math.cos(rads) + y * Math.sin(rads); + // var newY = y * Math.cos(rads) - x * Math.sin(rads); + + // return { + // x: newX, + // y: newY, + // rads: rads, + // angle: angle + // }; + // }; + // } + // this.save(); // rotation + // this.rotate(90, {origin: [lastPositionX, startY]}); + // width = 50; + + // background header + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: width, + height: this.headerHeight + columnSpacing, + }; + + // add background + this.addBackground(rectCell, headerColor, headerOpacity); + + // cell padding + cellPadding = prepareCellPadding(padding || options.padding || 0); + + // write + this.text(label, + lastPositionX + (cellPadding.left), + startY, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }) + + lastPositionX += width; + // this.restore(); // rotation + + }); + + } + + // set style + prepareRowOptions(table.headers); + + } + + if(!options.hideHeader) { + // Refresh the y coordinate of the bottom of the headers row + rowBottomY = Math.max(startY + computeRowHeight(table.headers, true), rowBottomY); + // Separation line between headers and rows + separationsRow('header', startX, rowBottomY); + } else { + rowBottomY = startY; + } + + }; + + // End header + addHeader(); + + // Datas + table.datas.forEach((row, i) => { + + this.datasIndex = i; + const rowHeight = computeRowHeight(row, false); + this.logg(rowHeight); + + // Switch to next page if we cannot go any further because the space is over. + // For safety, consider 3 rows margin instead of just one + // if (startY + 2 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + // else this.emitter.emit('addPage'); //this.addPage(); + console.log('plugin datas', this.y, this.y + safelyMarginBottom + rowHeight) + if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // calc position + startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + + // unlock add page function + lockAddPage = false; + + const rectRow = { + x: startX, + y: startY - columnSpacing - (rowDistance * 2), + width: tableWidth - startX, + height: rowHeight + columnSpacing, + }; + + // add background row + prepareRowBackground(row, rectRow); + + lastPositionX = startX; + + // Print all cells of the current row + table.headers.forEach(( dataHeader, index) => { + + let {property, width, renderer, align, valign, padding} = dataHeader; + + // check defination + width = width || columnWidth; + align = align || 'left'; + + // cell padding + cellPadding = prepareCellPadding(padding || options.padding || 0); + + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: width, + height: rowHeight + columnSpacing, + } + + // allow the user to override style for rows + prepareRowOptions(row); + prepareRow(row, index, i, rectRow, rectCell,); + + let text = row[property]; + + // cell object + if(typeof text === 'object' ){ + + text = String(text.label); // get label + // row[property].hasOwnProperty('options') && prepareRowOptions(row[property]); // set style + + // options if text cell is object + if( row[property].hasOwnProperty('options') ){ + + // set font style + prepareRowOptions(row[property]); + prepareRowBackground(row[property], rectCell); + + } + + } else { + + // style column by header + prepareRowBackground(table.headers[index], rectCell); + + } + + // bold + if( String(text).indexOf('bold:') === 0 ){ + this.font('Helvetica-Bold'); + text = text.replace('bold:',''); + } + + // size + if( String(text).indexOf('size') === 0 ){ + let size = String(text).substr(4,2).replace(':','').replace('+','') >> 0; + this.fontSize( size < 7 ? 7 : size ); + text = text.replace(`size${size}:`,''); + } + + // renderer column + // renderer && (text = renderer(text, index, i, row, rectRow, rectCell)) // value, index-column, index-row, row nbhmn + if(typeof renderer === 'function'){ + text = renderer(text, index, i, row, rectRow, rectCell); // value, index-column, index-row, row, doc[this] + } + + // TODO # Experimental + // ------------------------------------------------------------------------------ + // align vertically + let topTextToAlignVertically = 0; + if(valign && valign !== 'top'){ + const heightText = this.heightOfString(text, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }); + // line height, spacing hehight, cell and text diference + topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; + } + // ------------------------------------------------------------------------------ + + this.text(text, + lastPositionX + (cellPadding.left), + startY + topTextToAlignVertically, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }); + + lastPositionX += width; + + // set style + // Maybe REMOVE ??? + prepareRowOptions(row); + prepareRow(row, index, i, rectRow, rectCell); + + }); + + // Refresh the y coordinate of the bottom of this row + rowBottomY = Math.max(startY + rowHeight, rowBottomY); + + // console.log(this.page.height, rowBottomY, this.y); + // text is so big as page (crazy!) + if(rowBottomY > this.page.height) { + rowBottomY = this.y + columnSpacing + (rowDistance * 2); + } + + // Separation line between rows + separationsRow('horizontal', startX, rowBottomY); + + // review this code + if( row.hasOwnProperty('options') ){ + if( row.options.hasOwnProperty('separation') ){ + // Separation line between rows + separationsRow('horizontal',startX, rowBottomY, 1, 1); + } + } + + }); + // End datas + + // Rows + table.rows.forEach((row, i) => { + + this.rowsIndex = i; + const rowHeight = computeRowHeight(row, false); + this.logg(rowHeight); + + // Switch to next page if we cannot go any further because the space is over. + // For safety, consider 3 rows margin instead of just one + // if (startY + 3 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + // else this.emitter.emit('addPage'); //this.addPage(); + if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // calc position + startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + + // unlock add page function + lockAddPage = false; + + const rectRow = { + x: columnPositions[0], + // x: startX, + y: startY - columnSpacing - (rowDistance * 2), + width: tableWidth - startX, + height: rowHeight + columnSpacing, + } + + // add background + // doc.addBackground(rectRow); + + lastPositionX = startX; + + row.forEach((cell, index) => { + + let align = 'left'; + let valign = undefined; + + const rectCell = { + // x: columnPositions[index], + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: columnSizes[index], + height: rowHeight + columnSpacing, + } + + prepareRowBackground(table.headers[index], rectCell); + + // Allow the user to override style for rows + prepareRow(row, index, i, rectRow, rectCell); + + if(typeof table.headers[index] === 'object') { + // renderer column + table.headers[index].renderer && (cell = table.headers[index].renderer(cell, index, i, row, rectRow, rectCell, this)); // text-cell, index-column, index-line, row, doc[this] + // align + table.headers[index].align && (align = table.headers[index].align); + table.headers[index].valign && (valign = table.headers[index].valign); + } + + // cell padding + cellPadding = prepareCellPadding(table.headers[index].padding || options.padding || 0); + + // TODO # Experimental + // ------------------------------------------------------------------------------ + // align vertically + let topTextToAlignVertically = 0; + if(valign && valign !== 'top'){ + const heightText = this.heightOfString(cell, { + width: columnSizes[index] - (cellPadding.left + cellPadding.right), + align: align, + }); + // line height, spacing hehight, cell and text diference + topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; + } + // ------------------------------------------------------------------------------ + + this.text(cell, + lastPositionX + (cellPadding.left), + startY + topTextToAlignVertically, { + width: columnSizes[index] - (cellPadding.left + cellPadding.right), + align: align, + }); + + lastPositionX += columnSizes[index]; + + }); + + // Refresh the y coordinate of the bottom of this row + rowBottomY = Math.max(startY + rowHeight, rowBottomY); + + // console.log(this.page.height, rowBottomY, this.y); + // text is so big as page (crazy!) + if(rowBottomY > this.page.height) { + rowBottomY = this.y + columnSpacing + (rowDistance * 2); + } + + // Separation line between rows + separationsRow('horizontal', startX, rowBottomY); + + }); + // End rows + + // update position + this.x = startX; + this.y = rowBottomY; // position y final; + this.moveDown(); // break + + // add fire + this.off("pageAdded", onFirePageAdded); + + // callback + typeof callback === 'function' && callback(this); + + // nice :) + resolve(); + + } catch (error) { + + // error + reject(error); + + } + + }); + } + + /** + * tables + * @param {Object} tables + * @returns + */ + async tables(tables, callback) { + return new Promise(async (resolve, reject) => { + try { + + if(Array.isArray(tables) === false) + { + resolve(); + return; + } + + const len = tables.length; + for(let i; i < len; i++) + { + await this.table(tables[i], tables[i].options || {}); + } + + // if tables is Array + // Array.isArray(tables) ? + // // for each on Array + // tables.forEach( async table => await this.table( table, table.options || {} ) ) : + // // else is tables is a unique table object + // ( typeof tables === 'object' ? this.table( tables, tables.options || {} ) : null ) ; + // // callback + typeof callback === 'function' && callback(this); + // // donw! + resolve(); + } + catch(error) + { + reject(error); + } + + }); + } + +} + +module.exports = PDFDocumentWithTables; +module.exports.default = PDFDocumentWithTables; From 914aa77449b2b0bbe0475d72eeae2158c033aff1 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 15:42:50 +0300 Subject: [PATCH 15/18] fixing namings to match typescript --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 67b8f45..864da6f 100644 --- a/index.js +++ b/index.js @@ -9,12 +9,12 @@ class PDFDocumentWithTables extends PDFDocument { this.renderCallbacksOnNewPage = []; } - queueRenderOnNewPage(renderFn, callback) { + queueRenderOnAddPage(renderFn, callback) { this.renderCallbacksOnNewPage.push(renderFn); if (typeof callback === "function") callback(this); } - addRectBackground({ x, y, width, height }, fillColor = "grey", fillOpacity = 0.1, callback) { + addBackground({ x, y, width, height }, fillColor = "grey", fillOpacity = 0.1, callback) { this.save() .fill(fillColor) .fillOpacity(fillOpacity) @@ -25,7 +25,7 @@ class PDFDocumentWithTables extends PDFDocument { if (typeof callback === "function") callback(this); } - async renderTable(tableData, userOptions, callback) { + async table(tableData, userOptions, callback) { return new Promise((resolve, reject) => { try { if (typeof tableData === "string") tableData = JSON.parse(tableData); From 16481c2fa30a36b95516f6ee583a4cd99ded8e32 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 15:54:22 +0300 Subject: [PATCH 16/18] fixing compute row height function --- index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 864da6f..644de65 100644 --- a/index.js +++ b/index.js @@ -189,7 +189,15 @@ class PDFDocumentWithTables extends PDFDocument { const computeRowHeight = (row, isHeader) => { let maxHeight = isHeader ? 0 : options.minRowHeight || 0; - row.forEach((cell, i) => { + let rowData = row; + + if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ + const cells = []; + table.headers.forEach(({property}) => cells.push(row[property]) ); + rowData = cells; + } + + rowData.forEach((cell, i) => { let text = typeof cell === "object" ? String(cell.label ?? "") : String(cell); const pad = parsePadding(tableData.headers[i]?.padding ?? options.padding); const height = this.heightOfString(text, { From 8a4ad57d2bd923529899a49ba233eba6355bfe7e Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 15:57:23 +0300 Subject: [PATCH 17/18] fix wrong variable name --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 644de65..85cb667 100644 --- a/index.js +++ b/index.js @@ -193,7 +193,7 @@ class PDFDocumentWithTables extends PDFDocument { if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ const cells = []; - table.headers.forEach(({property}) => cells.push(row[property]) ); + tableData.headers.forEach(({property}) => cells.push(row[property]) ); rowData = cells; } From 2e280aa2cb8995e3ea7c4c9bd352338e0dd38f29 Mon Sep 17 00:00:00 2001 From: mar10-emil Date: Sun, 26 Oct 2025 16:41:06 +0300 Subject: [PATCH 18/18] reverting back to old implementation --- index.js | 1103 +++++++++++++++++++++++++++++++++++++++-------- legacy-index.js | 981 ----------------------------------------- 2 files changed, 930 insertions(+), 1154 deletions(-) delete mode 100644 legacy-index.js diff --git a/index.js b/index.js index 85cb667..a781105 100644 --- a/index.js +++ b/index.js @@ -1,224 +1,981 @@ +// jshint esversion: 6 +// "use strict"; +// https://jshint.com/ + const PDFDocument = require("pdfkit"); +// const EventEmitter = require('events').EventEmitter; class PDFDocumentWithTables extends PDFDocument { - renderCallbacksOnNewPage; - - constructor(options) { - super(options); - this.options = options; - this.renderCallbacksOnNewPage = []; + rendersOnAddPage; + + constructor(option) { + super(option); + this.opt = option; + this.rendersOnAddPage = [] + // this.emitter = new EventEmitter(); } - queueRenderOnAddPage(renderFn, callback) { - this.renderCallbacksOnNewPage.push(renderFn); - if (typeof callback === "function") callback(this); + /** + * queueRenderOnAddPage + * @param {(doc: PDFDocumentWithTables) => void} section + */ + queueRenderOnAddPage(section, callback) { + this.rendersOnAddPage.push(section) + typeof callback === 'function' && callback(this); } - addBackground({ x, y, width, height }, fillColor = "grey", fillOpacity = 0.1, callback) { - this.save() - .fill(fillColor) - .fillOpacity(fillOpacity) - .rect(x, y, width, height) - .fill() - .restore(); - if (typeof callback === "function") callback(this); + logg(...args) { + // console.log(args); + } + + /** + * addBackground + * @param {Object} rect + * @param {String} fillColor + * @param {Number} fillOpacity + * @param {Function} callback + */ + addBackground ({x, y, width, height}, fillColor, fillOpacity, callback) { + + // validate + fillColor || (fillColor = 'grey'); + fillOpacity || (fillOpacity = 0.1); + + // save current style + this.save(); + + // draw bg + this + .fill(fillColor) + //.stroke(fillColor) + .fillOpacity(fillOpacity) + .rect( x, y, width, height ) + //.stroke() + .fill(); + + // back to saved style + this.restore(); + + // restore + // this + // .fillColor('black') + // .fillOpacity(1) + // .fill(); + + typeof callback === 'function' && callback(this); + } - async table(tableData, userOptions, callback) { + /** + * table + * @param {Object} table + * @param {Object} options + * @param {Function} callback + */ + table(table, options, callback) { return new Promise((resolve, reject) => { try { - if (typeof tableData === "string") tableData = JSON.parse(tableData); - if (!tableData) tableData = {}; - if (!userOptions) userOptions = {}; - - tableData.headers ??= []; - tableData.datas ??= []; - tableData.rows ??= []; - if (tableData.options) userOptions = { ...userOptions, ...tableData.options }; - - const defaults = { - hideHeader: false, - padding: 0, - columnsSize: [], - addPage: false, - absolutePosition: false, - minRowHeight: 0, - divider: { - header: { disabled: false }, - horizontal: { disabled: false }, - vertical: { disabled: true }, - }, + + typeof table === 'string' && (table = JSON.parse(table)); + + table || (table = {}); + options || (options = {}); + + table.headers || (table.headers = []); + table.datas || (table.datas = []); + table.rows || (table.rows = []); + table.options && (options = {...options, ...table.options}); + + options.hideHeader || (options.hideHeader = false); + options.padding || (options.padding = 0); + options.columnsSize || (options.columnsSize = []); + options.addPage || (options.addPage = false); + options.absolutePosition || (options.absolutePosition = false); + options.minRowHeight || (options.minRowHeight = 0); + // TODO options.hyperlink || (options.hyperlink = { urlToLink: false, description: null }); + + // divider lines + options.divider || (options.divider = {}); + options.divider.header || (options.divider.header = { disabled: false, width: undefined, opacity: undefined }); + options.divider.horizontal || (options.divider.horizontal = { disabled: false, width: undefined, opacity: undefined }); + options.divider.vertical || (options.divider.vertical = { disabled: true, width: undefined, opacity: undefined }); + + if(!table.headers.length) throw new Error('Headers not defined. Use options: hideHeader to hide.'); + + if(options.useSafelyMarginBottom === undefined) options.useSafelyMarginBottom = true; + + const title = table.title ? table.title : ( options.title || '' ) ; + const subtitle = table.subtitle ? table.subtitle : ( options.subtitle || '' ) ; + + this.logg('layout', this.page.layout); + this.logg('size', this.page.size); + this.logg('margins', this.page.margins); + // this.logg('options', this.options); + + // const columnIsDefined = options.columnsSize.length ? true : false; + const columnSpacing = options.columnSpacing || 3; // 15 + let columnSizes = []; + let columnPositions = []; // 0, 10, 20, 30, 100 + let columnWidth = 0; + + const rowDistance = 0.5; + let cellPadding = {top: 0, right: 0, bottom: 0, left: 0}; // universal + + const prepareHeader = options.prepareHeader || (() => this.fillColor('black').font("Helvetica-Bold").fontSize(8).fill()); + const prepareRow = options.prepareRow || ((row, indexColumn, indexRow, rectRow, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); + //const prepareCell = options.prepareCell || ((cell, indexColumn, indexRow, indexCell, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); + + let tableWidth = 0; + const maxY = this.page.height - (this.page.margins.bottom); // this.page.margins.top + + + let startX = options.x || this.x || this.page.margins.left; + let startY = options.y || this.y || this.page.margins.top; + + let lastPositionX = 0; + let rowBottomY = 0; + + //------------ experimental fast variables + let titleHeight = 0; + this.headerHeight = 0; + let firstLineHeight = 0; + this.datasIndex = 0; + this.rowsIndex = 0 ; + let lockAddTitles = false; // to addd title one time + let lockAddPage = false; + let lockAddHeader = false; + let safelyMarginBottom = this.page.margins.top/2; + + // reset position to margins.left + if( options.x === null || options.x === -1 ){ + startX = this.page.margins.left; + } + + const createTitle = ( data, size, opacity ) => { + + // Title + if(!data) return; + + // get height line + // let cellHeight = 0; + // if string + if(typeof data === 'string' ){ + // font size + this.fillColor('black').fontSize(8).fontSize(size).opacity(opacity).fill(); + // this.fillColor('black').font("Helvetica").fontSize(8).fontSize(size).opacity(opacity).fill(); + + // const titleHeight = this.heightOfString(data, { + // width: tableWidth, + // align: 'left', + // }); + this.logg(data, titleHeight); // 24 + + // write + this.text( data, startX, startY ).opacity( 1 ); // moveDown( 0.5 ) + // startY += cellHeight; + startY = this.y + columnSpacing + 2; + // else object + } else if(typeof data === 'object' ){ + // title object + data.fontFamily && this.font( data.fontFamily ); + data.label && this.fillColor( data.color || 'black').fontSize( data.fontSize || size ).text( data.label, startX, startY ).fill(); + + startY = this.y + columnSpacing + 2; + + } }; - const options = { ...defaults, ...userOptions }; - - if (!tableData.headers.length && !options.hideHeader) - throw new Error("Headers not defined. Use hideHeader option to skip."); - - const title = tableData.title || options.title || ""; - const subtitle = tableData.subtitle || options.subtitle || ""; - const columnSpacing = options.columnSpacing || 3; - let columnSizes = []; - let columnPositions = []; - let tableWidth = 0; - let lastX = 0; - let startX = options.x ?? this.page.margins.left; - let startY = options.y ?? this.page.margins.top; - const maxY = this.page.height - this.page.margins.bottom; - const safeBottomMargin = this.page.margins.top / 2; - - const prepareHeader = options.prepareHeader || (() => this.fillColor("black").font("Helvetica-Bold").fontSize(8)); - const prepareRow = options.prepareRow || (() => this.fillColor("black").font("Helvetica").fontSize(8)); - - const onNewPage = () => { + + // add a new page before crate table + options.addPage === true && onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // // create title and subtitle + // createTitle( title, 12, 1 ); + // createTitle( subtitle, 9, 0.7 ); + + // add space after title + // if( title || subtitle ){ + // startY += 3; + // }; + + // event emitter + const onFirePageAdded = () => { + // startX = this.page.margins.left; startY = this.page.margins.top; - this.addPage({ + rowBottomY = 0; + // lockAddPage || this.addPage(this.options); + lockAddPage || this.addPage({ layout: this.page.layout, size: this.page.size, margins: this.page.margins, }); - this.renderCallbacksOnNewPage.forEach((fn) => fn(this)); - addTableHeader(); + this.rendersOnAddPage.forEach(section => section(this)) + lockAddHeader || addHeader(); + //addHeader(); }; - - const evalFunction = (code) => { - let fn = null; - eval("fn = " + code); - return fn; + + // add fire + // this.emitter.removeAllListeners(); + // this.emitter.on('addTitle', addTitle); + // this.emitter.on('addSubtitle', addSubTitle); + // this.emitter.on('addPage', onFirePageAdded); + // this.emitter.emit('addPage'); + // this.on('pageAdded', onFirePageAdded); + + // warning - eval can be harmful + const fEval = (str) => { + let f = null; eval('f = ' + str); return f; }; - - const parsePadding = (padding) => { - if (Array.isArray(padding)) { - if (padding.length === 2) padding = [...padding, ...padding]; - else if (padding.length === 1) padding = Array(4).fill(padding[0]); - } else if (typeof padding === "number") { - padding = Array(4).fill(padding); - } else if (typeof padding === "object") { - const { top, right, bottom, left } = padding; - padding = [top, right, bottom, left]; - } else { - padding = [0, 0, 0, 0]; + + const separationsColumn = () => { + // soon + } + + const separationsRow = (type, x, y, width, opacity, color) => { + + type || (type = 'horizontal'); // header | horizontal | vertical + + // distance + const d = rowDistance * 1.5; + // margin + const m = options.x || this.page.margins.left || 30; + // disabled + const s = options.divider[type].disabled || false; + + if(s === true) return; + opacity = opacity || options.divider[type].opacity || 0.5; + width = width || options.divider[type].width || 0.5; + color = color || options.divider[type].color || 'black'; + + // draw + this + .moveTo(x, y - d) + .lineTo(x + tableWidth - m, y - d) + .lineWidth(width) + .strokeColor(color) + .opacity(opacity) + .stroke() + // Reset opacity after drawing the line + .opacity(1); + + }; + + // padding: [10, 10, 10, 10] + // padding: [10, 10] + // padding: {top: 10, right: 10, bottom: 10, left: 10} + // padding: 10, + const prepareCellPadding = (p) => { + + // array + if(Array.isArray(p)){ + switch(p.length){ + case 3: p = [...p, 0]; break; + case 2: p = [...p, ...p]; break; + case 1: p = Array(4).fill(p[0]); break; + } + } + // number + else if(typeof p === 'number'){ + p = Array(4).fill(p); } - return { top: padding[0], right: padding[1], bottom: padding[2], left: padding[3] }; + // object + else if(typeof p === 'object'){ + const {top, right, bottom, left} = p; + p = [top, right, bottom, left]; + } + // null + else { + p = Array(4).fill(0); + } + + return { + top: p[0] >> 0, // int + right: p[1] >> 0, + bottom: p[2] >> 0, + left: p[3] >> 0, + }; + }; - - const drawDivider = (type, x, y, width, opacity, color) => { - const divider = options.divider[type] ?? {}; - if (divider.disabled) return; - - const strokeOpacity = opacity ?? divider.opacity ?? 0.5; - const strokeWidth = width ?? divider.width ?? 0.5; - const strokeColor = color ?? divider.color ?? "black"; - - this.moveTo(x, y - 0.75) - .lineTo(x + tableWidth - startX, y - 0.75) - .lineWidth(strokeWidth) - .strokeColor(strokeColor) - .opacity(strokeOpacity) - .stroke() - .opacity(1); + + const prepareRowOptions = (row) => { + + // validate + if( typeof row !== 'object' || !row.hasOwnProperty('options') ) return; + + const {fontFamily, fontSize, color} = row.options; + + fontFamily && this.font(fontFamily); + fontSize && this.fontSize(fontSize); + color && this.fillColor(color); + + // row.options.hasOwnProperty('fontFamily') && this.font(row.options.fontFamily); + // row.options.hasOwnProperty('fontSize') && this.fontSize(row.options.fontSize); + // row.options.hasOwnProperty('color') && this.fillColor(row.options.color); + }; - - const setRowStyles = (row) => { - if (typeof row !== "object" || !row.options) return; - const { fontFamily, fontSize, color } = row.options; - if (fontFamily) this.font(fontFamily); - if (fontSize) this.fontSize(fontSize); - if (color) this.fillColor(color); + + const prepareRowBackground = (row, rect) => { + + // validate + if(typeof row !== 'object') return; + + // options + row.options && (row = row.options); + + let { fill, opac } = {}; + + // add backgroundColor + if(row.hasOwnProperty('columnColor')){ // ^0.1.70 + + const { columnColor, columnOpacity } = row; + fill = columnColor; + opac = columnOpacity; + + } else if(row.hasOwnProperty('backgroundColor')){ // ~0.1.65 old + + const { backgroundColor, backgroundOpacity } = row; + fill = backgroundColor; + opac = backgroundOpacity; + + } else if(row.hasOwnProperty('background')){ // dont remove + + if(typeof row.background === 'object'){ + let { color, opacity } = row.background; + fill = color; + opac = opacity; + } + + } + + fill && this.addBackground(rect, fill, opac); + }; + + const computeRowHeight = (row, isHeader) => { + + let result = isHeader ? 0 : (options.minRowHeight || 0); + let cellp; + + // if row is object, content with property and options + if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ + const cells = []; + // get all properties names on header + table.headers.forEach(({property}) => cells.push(row[property]) ); + // define row with properties header + row = cells; + } + + row.forEach((cell,i) => { + + let text = cell; + + // object + // read cell and get label of object + if( typeof cell === 'object' ){ + // define label + text = String(cell.label); + // apply font size on calc about height row + cell.hasOwnProperty('options') && prepareRowOptions(cell); + } + + text = String(text).replace('bold:','').replace('size',''); + + // cell padding + cellp = prepareCellPadding(table.headers[i].padding || options.padding || 0); + // cellp = prepareCellPadding(options.padding || 0); + // - (cellp.left + cellp.right + (columnSpacing * 2)) + // console.log(cellp); + + // calc height size of string + const cellHeight = this.heightOfString(text, { + width: columnSizes[i] - (cellp.left + cellp.right), + align: 'left', + }); + + result = Math.max(result, cellHeight); + }); - const drawRowBackground = (row, rect) => { - if (typeof row !== "object") return; - const opts = row.options ?? row; - const color = opts.columnColor ?? opts.backgroundColor ?? opts.background?.color; - const opacity = opts.columnOpacity ?? opts.backgroundOpacity ?? opts.background?.opacity; - if (color) this.addRectBackground(rect, color, opacity); - }; + // isHeader && (result = Math.max(result, options.minRowHeight)); - const calcColumnLayout = () => { - const headerWidths = tableData.headers.map((h) => h.width).filter(Boolean); - const tableMaxWidth = options.width - ? parseInt(options.width) || this.page.width - this.page.margins.right - startX - : this.page.width - this.page.margins.right - startX; + // if(result + columnSpacing === 0) { + // computeRowHeight(row); + // } + + return result + (columnSpacing); + }; + + // Calc columns size + + const calcColumnSizes = () => { + + let h = []; // header width + let p = []; // position + let w = 0; // table width + + // (table width) 1o - Max size table + w = this.page.width - this.page.margins.right - ( options.x || this.page.margins.left ); + // (table width) 2o - Size defined + options.width && ( w = parseInt(options.width) || String(options.width).replace(/[^0-9]/g,'') >> 0 ); + + // (table width) if table is percent of page + // ... + + // (size columns) 1o + table.headers.forEach( el => { + el.width && h.push(el.width); // - columnSpacing + }); + // (size columns) 2o + if(h.length === 0) { + h = options.columnsSize; + } + // (size columns) 3o + if(h.length === 0) { + columnWidth = ( w / table.headers.length ); // - columnSpacing // define column width + table.headers.forEach( () => h.push(columnWidth) ); + } + + // Set columnPositions + h.reduce((prev, curr, indx) => { + p.push(prev >> 0); + return prev + curr; + },( options.x || this.page.margins.left )); + + // !Set columnSizes + h.length && (columnSizes = h); + p.length && (columnPositions = p); + + // (table width) 3o - Sum last position + lest header width + w = p[p.length-1] + h[h.length-1]; + + // !Set tableWidth + w && ( tableWidth = w ); + + // Ajust spacing + // tableWidth = tableWidth - (h.length * columnSpacing); + + this.logg('columnSizes', h); + this.logg('columnPositions', p); + + }; + + calcColumnSizes(); + + // Header + + const addHeader = () => { + + // Allow the user to override style for headers + prepareHeader(); + + // calc header height + if(this.headerHeight === 0){ + this.headerHeight = computeRowHeight(table.headers, true); + this.logg(this.headerHeight, 'headers'); + } - if (!headerWidths.length) { - if (options.columnsSize.length) headerWidths.push(...options.columnsSize); - else { - const defaultWidth = tableMaxWidth / tableData.headers.length; - for (let i = 0; i < tableData.headers.length; i++) headerWidths.push(defaultWidth); + // calc first table line when init table + if(firstLineHeight === 0){ + if(table.datas.length > 0){ + firstLineHeight = computeRowHeight(table.datas[0], true); + this.logg(firstLineHeight, 'datas'); + } + if(table.rows.length > 0){ + firstLineHeight = computeRowHeight(table.rows[0], true); + this.logg(firstLineHeight, 'rows'); } } - columnSizes = headerWidths; - columnPositions = headerWidths.reduce((acc, width, i) => { - acc.push((acc[i - 1] ?? startX) + width); - return acc; - }, []); + // 24.1 is height calc title + subtitle + titleHeight = !lockAddTitles ? 24.1 : 0; + // calc if header + first line fit on last page + const calc = startY + titleHeight + firstLineHeight + this.headerHeight + safelyMarginBottom// * 1.3; + + // content is big text (crazy!) + if(firstLineHeight > maxY) { + // lockAddHeader = true; + lockAddPage = true; + this.logg('CRAZY! This a big text on cell'); + } else if(calc > maxY) { // && !lockAddPage + // lockAddHeader = false; + lockAddPage = false; + onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + return; + } - tableWidth = columnPositions[columnPositions.length - 1]; - }; + // if has title + if(lockAddTitles === false) { + + // create title and subtitle + createTitle( title, 12, 1 ); + createTitle( subtitle, 9, 0.7 ); + + // add space after title + if( title || subtitle ){ + startY += 3; + }; - const addTableHeader = () => { - prepareHeader(); - const headerHeight = computeRowHeight(tableData.headers, true); - const firstRowHeight = - (tableData.datas.length && computeRowHeight(tableData.datas[0], false)) || - (tableData.rows.length && computeRowHeight(tableData.rows[0], false)) || - 0; - - if (startY + headerHeight + firstRowHeight + safeBottomMargin > maxY) { - onNewPage(); - return; } - if (!options.hideHeader && tableData.headers.length) { - tableData.headers.forEach((header, i) => { - const rect = { x: startX + i * columnSizes[i], y: startY, width: columnSizes[i], height: headerHeight }; - this.addRectBackground(rect, "lightgray", 0.2); + // Allow the user to override style for headers + prepareHeader(); + + lockAddTitles = true; - const pad = parsePadding(options.padding); - const text = typeof header === "string" ? header : header.label; - this.text(text, rect.x + pad.left, startY, { - width: rect.width - pad.left - pad.right, - align: "left", + // this options is trial + if(options.absolutePosition === true){ + lastPositionX = options.x || startX || this.x; // x position head + startY = options.y || startY || this.y; // x position head + } else { + lastPositionX = startX; // x position head + } + + // Check to have enough room for header and first rows. default 3 + // if (startY + 2 * this.headerHeight >= maxY) this.emitter.emit('addPage'); //this.addPage(); + + if(!options.hideHeader && table.headers.length > 0) { + + // simple header + if(typeof table.headers[0] === 'string') { + + // // background header + // const rectRow = { + // x: startX, + // y: startY - columnSpacing - (rowDistance * 2), + // width: columnWidth, + // height: this.headerHeight + columnSpacing, + // }; + + // // add background + // this.addBackground(rectRow); + + // print headers + table.headers.forEach((header, i) => { + + // background header + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: columnSizes[i], + height: this.headerHeight + columnSpacing, + }; + + // add background + this.addBackground(rectCell); + + // cell padding + cellPadding = prepareCellPadding(options.padding || 0); + + // write + this.text(header, + lastPositionX + (cellPadding.left), + startY, { + width: Number(columnSizes[i]) - (cellPadding.left + cellPadding.right), + align: 'left', + }); + + lastPositionX += columnSizes[i] >> 0; + }); - }); - startY += headerHeight; + + }else{ + + // Print all headers + table.headers.forEach( (dataHeader, i) => { + + let {label, width, renderer, align, headerColor, headerOpacity, headerAlign, padding} = dataHeader; + // check defination + width = width || columnSizes[i]; + align = headerAlign || align || 'left'; + // force number + width = width >> 0; + + // register renderer function + if(renderer && typeof renderer === 'string') { + table.headers[i].renderer = fEval(renderer); + } + + // # Rotation + // var doTransform = function (x, y, angle) { + // var rads = angle / 180 * Math.PI; + // var newX = x * Math.cos(rads) + y * Math.sin(rads); + // var newY = y * Math.cos(rads) - x * Math.sin(rads); + + // return { + // x: newX, + // y: newY, + // rads: rads, + // angle: angle + // }; + // }; + // } + // this.save(); // rotation + // this.rotate(90, {origin: [lastPositionX, startY]}); + // width = 50; + + // background header + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: width, + height: this.headerHeight + columnSpacing, + }; + + // add background + this.addBackground(rectCell, headerColor, headerOpacity); + + // cell padding + cellPadding = prepareCellPadding(padding || options.padding || 0); + + // write + this.text(label, + lastPositionX + (cellPadding.left), + startY, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }) + + lastPositionX += width; + // this.restore(); // rotation + + }); + + } + + // set style + prepareRowOptions(table.headers); + } + + if(!options.hideHeader) { + // Refresh the y coordinate of the bottom of the headers row + rowBottomY = Math.max(startY + computeRowHeight(table.headers, true), rowBottomY); + // Separation line between headers and rows + separationsRow('header', startX, rowBottomY); + } else { + rowBottomY = startY; + } + }; - - const computeRowHeight = (row, isHeader) => { - let maxHeight = isHeader ? 0 : options.minRowHeight || 0; - let rowData = row; + + // End header + addHeader(); + + // Datas + table.datas.forEach((row, i) => { + + this.datasIndex = i; + const rowHeight = computeRowHeight(row, false); + this.logg(rowHeight); + + // Switch to next page if we cannot go any further because the space is over. + // For safety, consider 3 rows margin instead of just one + // if (startY + 2 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + // else this.emitter.emit('addPage'); //this.addPage(); + console.log('plugin datas', this.y, this.y + safelyMarginBottom + rowHeight) + if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // calc position + startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + + // unlock add page function + lockAddPage = false; - if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ - const cells = []; - tableData.headers.forEach(({property}) => cells.push(row[property]) ); - rowData = cells; + const rectRow = { + x: startX, + y: startY - columnSpacing - (rowDistance * 2), + width: tableWidth - startX, + height: rowHeight + columnSpacing, + }; + + // add background row + prepareRowBackground(row, rectRow); + + lastPositionX = startX; + + // Print all cells of the current row + table.headers.forEach(( dataHeader, index) => { + + let {property, width, renderer, align, valign, padding} = dataHeader; + + // check defination + width = width || columnWidth; + align = align || 'left'; + + // cell padding + cellPadding = prepareCellPadding(padding || options.padding || 0); + + const rectCell = { + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: width, + height: rowHeight + columnSpacing, + } + + // allow the user to override style for rows + prepareRowOptions(row); + prepareRow(row, index, i, rectRow, rectCell,); + + let text = row[property]; + + // cell object + if(typeof text === 'object' ){ + + text = String(text.label); // get label + // row[property].hasOwnProperty('options') && prepareRowOptions(row[property]); // set style + + // options if text cell is object + if( row[property].hasOwnProperty('options') ){ + + // set font style + prepareRowOptions(row[property]); + prepareRowBackground(row[property], rectCell); + + } + + } else { + + // style column by header + prepareRowBackground(table.headers[index], rectCell); + + } + + // bold + if( String(text).indexOf('bold:') === 0 ){ + this.font('Helvetica-Bold'); + text = text.replace('bold:',''); + } + + // size + if( String(text).indexOf('size') === 0 ){ + let size = String(text).substr(4,2).replace(':','').replace('+','') >> 0; + this.fontSize( size < 7 ? 7 : size ); + text = text.replace(`size${size}:`,''); + } + + // renderer column + // renderer && (text = renderer(text, index, i, row, rectRow, rectCell)) // value, index-column, index-row, row nbhmn + if(typeof renderer === 'function'){ + text = renderer(text, index, i, row, rectRow, rectCell); // value, index-column, index-row, row, doc[this] + } + + // TODO # Experimental + // ------------------------------------------------------------------------------ + // align vertically + let topTextToAlignVertically = 0; + if(valign && valign !== 'top'){ + const heightText = this.heightOfString(text, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }); + // line height, spacing hehight, cell and text diference + topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; + } + // ------------------------------------------------------------------------------ + + this.text(text, + lastPositionX + (cellPadding.left), + startY + topTextToAlignVertically, { + width: width - (cellPadding.left + cellPadding.right), + align: align, + }); + + lastPositionX += width; + + // set style + // Maybe REMOVE ??? + prepareRowOptions(row); + prepareRow(row, index, i, rectRow, rectCell); + + }); + + // Refresh the y coordinate of the bottom of this row + rowBottomY = Math.max(startY + rowHeight, rowBottomY); + + // console.log(this.page.height, rowBottomY, this.y); + // text is so big as page (crazy!) + if(rowBottomY > this.page.height) { + rowBottomY = this.y + columnSpacing + (rowDistance * 2); + } + + // Separation line between rows + separationsRow('horizontal', startX, rowBottomY); + + // review this code + if( row.hasOwnProperty('options') ){ + if( row.options.hasOwnProperty('separation') ){ + // Separation line between rows + separationsRow('horizontal',startX, rowBottomY, 1, 1); + } } + + }); + // End datas + + // Rows + table.rows.forEach((row, i) => { + + this.rowsIndex = i; + const rowHeight = computeRowHeight(row, false); + this.logg(rowHeight); + + // Switch to next page if we cannot go any further because the space is over. + // For safety, consider 3 rows margin instead of just one + // if (startY + 3 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows + // else this.emitter.emit('addPage'); //this.addPage(); + if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); + + // calc position + startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - rowData.forEach((cell, i) => { - let text = typeof cell === "object" ? String(cell.label ?? "") : String(cell); - const pad = parsePadding(tableData.headers[i]?.padding ?? options.padding); - const height = this.heightOfString(text, { - width: columnSizes[i] - pad.left - pad.right, - align: "left", + // unlock add page function + lockAddPage = false; + + const rectRow = { + x: columnPositions[0], + // x: startX, + y: startY - columnSpacing - (rowDistance * 2), + width: tableWidth - startX, + height: rowHeight + columnSpacing, + } + + // add background + // doc.addBackground(rectRow); + + lastPositionX = startX; + + row.forEach((cell, index) => { + + let align = 'left'; + let valign = undefined; + + const rectCell = { + // x: columnPositions[index], + x: lastPositionX, + y: startY - columnSpacing - (rowDistance * 2), + width: columnSizes[index], + height: rowHeight + columnSpacing, + } + + prepareRowBackground(table.headers[index], rectCell); + + // Allow the user to override style for rows + prepareRow(row, index, i, rectRow, rectCell); + + if(typeof table.headers[index] === 'object') { + // renderer column + table.headers[index].renderer && (cell = table.headers[index].renderer(cell, index, i, row, rectRow, rectCell, this)); // text-cell, index-column, index-line, row, doc[this] + // align + table.headers[index].align && (align = table.headers[index].align); + table.headers[index].valign && (valign = table.headers[index].valign); + } + + // cell padding + cellPadding = prepareCellPadding(table.headers[index].padding || options.padding || 0); + + // TODO # Experimental + // ------------------------------------------------------------------------------ + // align vertically + let topTextToAlignVertically = 0; + if(valign && valign !== 'top'){ + const heightText = this.heightOfString(cell, { + width: columnSizes[index] - (cellPadding.left + cellPadding.right), + align: align, + }); + // line height, spacing hehight, cell and text diference + topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; + } + // ------------------------------------------------------------------------------ + + this.text(cell, + lastPositionX + (cellPadding.left), + startY + topTextToAlignVertically, { + width: columnSizes[index] - (cellPadding.left + cellPadding.right), + align: align, }); - maxHeight = Math.max(maxHeight, height); + + lastPositionX += columnSizes[index]; + }); - return maxHeight + columnSpacing; - }; + + // Refresh the y coordinate of the bottom of this row + rowBottomY = Math.max(startY + rowHeight, rowBottomY); + + // console.log(this.page.height, rowBottomY, this.y); + // text is so big as page (crazy!) + if(rowBottomY > this.page.height) { + rowBottomY = this.y + columnSpacing + (rowDistance * 2); + } + + // Separation line between rows + separationsRow('horizontal', startX, rowBottomY); + + }); + // End rows + + // update position + this.x = startX; + this.y = rowBottomY; // position y final; + this.moveDown(); // break + + // add fire + this.off("pageAdded", onFirePageAdded); + + // callback + typeof callback === 'function' && callback(this); + + // nice :) + resolve(); + + } catch (error) { + + // error + reject(error); + + } + + }); + } - calcColumnLayout(); - addTableHeader(); + /** + * tables + * @param {Object} tables + * @returns + */ + async tables(tables, callback) { + return new Promise(async (resolve, reject) => { + try { - if (typeof callback === "function") callback(this); - resolve(this); - } catch (err) { - reject(err); + if(Array.isArray(tables) === false) + { + resolve(); + return; + } + + const len = tables.length; + for(let i; i < len; i++) + { + await this.table(tables[i], tables[i].options || {}); + } + + // if tables is Array + // Array.isArray(tables) ? + // // for each on Array + // tables.forEach( async table => await this.table( table, table.options || {} ) ) : + // // else is tables is a unique table object + // ( typeof tables === 'object' ? this.table( tables, tables.options || {} ) : null ) ; + // // callback + typeof callback === 'function' && callback(this); + // // donw! + resolve(); + } + catch(error) + { + reject(error); } + }); } + } module.exports = PDFDocumentWithTables; +module.exports.default = PDFDocumentWithTables; diff --git a/legacy-index.js b/legacy-index.js deleted file mode 100644 index a781105..0000000 --- a/legacy-index.js +++ /dev/null @@ -1,981 +0,0 @@ -// jshint esversion: 6 -// "use strict"; -// https://jshint.com/ - -const PDFDocument = require("pdfkit"); -// const EventEmitter = require('events').EventEmitter; - -class PDFDocumentWithTables extends PDFDocument { - rendersOnAddPage; - - constructor(option) { - super(option); - this.opt = option; - this.rendersOnAddPage = [] - // this.emitter = new EventEmitter(); - } - - /** - * queueRenderOnAddPage - * @param {(doc: PDFDocumentWithTables) => void} section - */ - queueRenderOnAddPage(section, callback) { - this.rendersOnAddPage.push(section) - typeof callback === 'function' && callback(this); - } - - - logg(...args) { - // console.log(args); - } - - /** - * addBackground - * @param {Object} rect - * @param {String} fillColor - * @param {Number} fillOpacity - * @param {Function} callback - */ - addBackground ({x, y, width, height}, fillColor, fillOpacity, callback) { - - // validate - fillColor || (fillColor = 'grey'); - fillOpacity || (fillOpacity = 0.1); - - // save current style - this.save(); - - // draw bg - this - .fill(fillColor) - //.stroke(fillColor) - .fillOpacity(fillOpacity) - .rect( x, y, width, height ) - //.stroke() - .fill(); - - // back to saved style - this.restore(); - - // restore - // this - // .fillColor('black') - // .fillOpacity(1) - // .fill(); - - typeof callback === 'function' && callback(this); - - } - - /** - * table - * @param {Object} table - * @param {Object} options - * @param {Function} callback - */ - table(table, options, callback) { - return new Promise((resolve, reject) => { - try { - - typeof table === 'string' && (table = JSON.parse(table)); - - table || (table = {}); - options || (options = {}); - - table.headers || (table.headers = []); - table.datas || (table.datas = []); - table.rows || (table.rows = []); - table.options && (options = {...options, ...table.options}); - - options.hideHeader || (options.hideHeader = false); - options.padding || (options.padding = 0); - options.columnsSize || (options.columnsSize = []); - options.addPage || (options.addPage = false); - options.absolutePosition || (options.absolutePosition = false); - options.minRowHeight || (options.minRowHeight = 0); - // TODO options.hyperlink || (options.hyperlink = { urlToLink: false, description: null }); - - // divider lines - options.divider || (options.divider = {}); - options.divider.header || (options.divider.header = { disabled: false, width: undefined, opacity: undefined }); - options.divider.horizontal || (options.divider.horizontal = { disabled: false, width: undefined, opacity: undefined }); - options.divider.vertical || (options.divider.vertical = { disabled: true, width: undefined, opacity: undefined }); - - if(!table.headers.length) throw new Error('Headers not defined. Use options: hideHeader to hide.'); - - if(options.useSafelyMarginBottom === undefined) options.useSafelyMarginBottom = true; - - const title = table.title ? table.title : ( options.title || '' ) ; - const subtitle = table.subtitle ? table.subtitle : ( options.subtitle || '' ) ; - - this.logg('layout', this.page.layout); - this.logg('size', this.page.size); - this.logg('margins', this.page.margins); - // this.logg('options', this.options); - - // const columnIsDefined = options.columnsSize.length ? true : false; - const columnSpacing = options.columnSpacing || 3; // 15 - let columnSizes = []; - let columnPositions = []; // 0, 10, 20, 30, 100 - let columnWidth = 0; - - const rowDistance = 0.5; - let cellPadding = {top: 0, right: 0, bottom: 0, left: 0}; // universal - - const prepareHeader = options.prepareHeader || (() => this.fillColor('black').font("Helvetica-Bold").fontSize(8).fill()); - const prepareRow = options.prepareRow || ((row, indexColumn, indexRow, rectRow, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); - //const prepareCell = options.prepareCell || ((cell, indexColumn, indexRow, indexCell, rectCell) => this.fillColor('black').font("Helvetica").fontSize(8).fill()); - - let tableWidth = 0; - const maxY = this.page.height - (this.page.margins.bottom); // this.page.margins.top + - - let startX = options.x || this.x || this.page.margins.left; - let startY = options.y || this.y || this.page.margins.top; - - let lastPositionX = 0; - let rowBottomY = 0; - - //------------ experimental fast variables - let titleHeight = 0; - this.headerHeight = 0; - let firstLineHeight = 0; - this.datasIndex = 0; - this.rowsIndex = 0 ; - let lockAddTitles = false; // to addd title one time - let lockAddPage = false; - let lockAddHeader = false; - let safelyMarginBottom = this.page.margins.top/2; - - // reset position to margins.left - if( options.x === null || options.x === -1 ){ - startX = this.page.margins.left; - } - - const createTitle = ( data, size, opacity ) => { - - // Title - if(!data) return; - - // get height line - // let cellHeight = 0; - // if string - if(typeof data === 'string' ){ - // font size - this.fillColor('black').fontSize(8).fontSize(size).opacity(opacity).fill(); - // this.fillColor('black').font("Helvetica").fontSize(8).fontSize(size).opacity(opacity).fill(); - - // const titleHeight = this.heightOfString(data, { - // width: tableWidth, - // align: 'left', - // }); - this.logg(data, titleHeight); // 24 - - // write - this.text( data, startX, startY ).opacity( 1 ); // moveDown( 0.5 ) - // startY += cellHeight; - startY = this.y + columnSpacing + 2; - // else object - } else if(typeof data === 'object' ){ - // title object - data.fontFamily && this.font( data.fontFamily ); - data.label && this.fillColor( data.color || 'black').fontSize( data.fontSize || size ).text( data.label, startX, startY ).fill(); - - startY = this.y + columnSpacing + 2; - - } - }; - - // add a new page before crate table - options.addPage === true && onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // // create title and subtitle - // createTitle( title, 12, 1 ); - // createTitle( subtitle, 9, 0.7 ); - - // add space after title - // if( title || subtitle ){ - // startY += 3; - // }; - - // event emitter - const onFirePageAdded = () => { - // startX = this.page.margins.left; - startY = this.page.margins.top; - rowBottomY = 0; - // lockAddPage || this.addPage(this.options); - lockAddPage || this.addPage({ - layout: this.page.layout, - size: this.page.size, - margins: this.page.margins, - }); - this.rendersOnAddPage.forEach(section => section(this)) - lockAddHeader || addHeader(); - //addHeader(); - }; - - // add fire - // this.emitter.removeAllListeners(); - // this.emitter.on('addTitle', addTitle); - // this.emitter.on('addSubtitle', addSubTitle); - // this.emitter.on('addPage', onFirePageAdded); - // this.emitter.emit('addPage'); - // this.on('pageAdded', onFirePageAdded); - - // warning - eval can be harmful - const fEval = (str) => { - let f = null; eval('f = ' + str); return f; - }; - - const separationsColumn = () => { - // soon - } - - const separationsRow = (type, x, y, width, opacity, color) => { - - type || (type = 'horizontal'); // header | horizontal | vertical - - // distance - const d = rowDistance * 1.5; - // margin - const m = options.x || this.page.margins.left || 30; - // disabled - const s = options.divider[type].disabled || false; - - if(s === true) return; - opacity = opacity || options.divider[type].opacity || 0.5; - width = width || options.divider[type].width || 0.5; - color = color || options.divider[type].color || 'black'; - - // draw - this - .moveTo(x, y - d) - .lineTo(x + tableWidth - m, y - d) - .lineWidth(width) - .strokeColor(color) - .opacity(opacity) - .stroke() - // Reset opacity after drawing the line - .opacity(1); - - }; - - // padding: [10, 10, 10, 10] - // padding: [10, 10] - // padding: {top: 10, right: 10, bottom: 10, left: 10} - // padding: 10, - const prepareCellPadding = (p) => { - - // array - if(Array.isArray(p)){ - switch(p.length){ - case 3: p = [...p, 0]; break; - case 2: p = [...p, ...p]; break; - case 1: p = Array(4).fill(p[0]); break; - } - } - // number - else if(typeof p === 'number'){ - p = Array(4).fill(p); - } - // object - else if(typeof p === 'object'){ - const {top, right, bottom, left} = p; - p = [top, right, bottom, left]; - } - // null - else { - p = Array(4).fill(0); - } - - return { - top: p[0] >> 0, // int - right: p[1] >> 0, - bottom: p[2] >> 0, - left: p[3] >> 0, - }; - - }; - - const prepareRowOptions = (row) => { - - // validate - if( typeof row !== 'object' || !row.hasOwnProperty('options') ) return; - - const {fontFamily, fontSize, color} = row.options; - - fontFamily && this.font(fontFamily); - fontSize && this.fontSize(fontSize); - color && this.fillColor(color); - - // row.options.hasOwnProperty('fontFamily') && this.font(row.options.fontFamily); - // row.options.hasOwnProperty('fontSize') && this.fontSize(row.options.fontSize); - // row.options.hasOwnProperty('color') && this.fillColor(row.options.color); - - }; - - const prepareRowBackground = (row, rect) => { - - // validate - if(typeof row !== 'object') return; - - // options - row.options && (row = row.options); - - let { fill, opac } = {}; - - // add backgroundColor - if(row.hasOwnProperty('columnColor')){ // ^0.1.70 - - const { columnColor, columnOpacity } = row; - fill = columnColor; - opac = columnOpacity; - - } else if(row.hasOwnProperty('backgroundColor')){ // ~0.1.65 old - - const { backgroundColor, backgroundOpacity } = row; - fill = backgroundColor; - opac = backgroundOpacity; - - } else if(row.hasOwnProperty('background')){ // dont remove - - if(typeof row.background === 'object'){ - let { color, opacity } = row.background; - fill = color; - opac = opacity; - } - - } - - fill && this.addBackground(rect, fill, opac); - - }; - - const computeRowHeight = (row, isHeader) => { - - let result = isHeader ? 0 : (options.minRowHeight || 0); - let cellp; - - // if row is object, content with property and options - if(!Array.isArray(row) && typeof row === 'object' && !row.hasOwnProperty('property')){ - const cells = []; - // get all properties names on header - table.headers.forEach(({property}) => cells.push(row[property]) ); - // define row with properties header - row = cells; - } - - row.forEach((cell,i) => { - - let text = cell; - - // object - // read cell and get label of object - if( typeof cell === 'object' ){ - // define label - text = String(cell.label); - // apply font size on calc about height row - cell.hasOwnProperty('options') && prepareRowOptions(cell); - } - - text = String(text).replace('bold:','').replace('size',''); - - // cell padding - cellp = prepareCellPadding(table.headers[i].padding || options.padding || 0); - // cellp = prepareCellPadding(options.padding || 0); - // - (cellp.left + cellp.right + (columnSpacing * 2)) - // console.log(cellp); - - // calc height size of string - const cellHeight = this.heightOfString(text, { - width: columnSizes[i] - (cellp.left + cellp.right), - align: 'left', - }); - - result = Math.max(result, cellHeight); - }); - - // isHeader && (result = Math.max(result, options.minRowHeight)); - - // if(result + columnSpacing === 0) { - // computeRowHeight(row); - // } - - return result + (columnSpacing); - }; - - // Calc columns size - - const calcColumnSizes = () => { - - let h = []; // header width - let p = []; // position - let w = 0; // table width - - // (table width) 1o - Max size table - w = this.page.width - this.page.margins.right - ( options.x || this.page.margins.left ); - // (table width) 2o - Size defined - options.width && ( w = parseInt(options.width) || String(options.width).replace(/[^0-9]/g,'') >> 0 ); - - // (table width) if table is percent of page - // ... - - // (size columns) 1o - table.headers.forEach( el => { - el.width && h.push(el.width); // - columnSpacing - }); - // (size columns) 2o - if(h.length === 0) { - h = options.columnsSize; - } - // (size columns) 3o - if(h.length === 0) { - columnWidth = ( w / table.headers.length ); // - columnSpacing // define column width - table.headers.forEach( () => h.push(columnWidth) ); - } - - // Set columnPositions - h.reduce((prev, curr, indx) => { - p.push(prev >> 0); - return prev + curr; - },( options.x || this.page.margins.left )); - - // !Set columnSizes - h.length && (columnSizes = h); - p.length && (columnPositions = p); - - // (table width) 3o - Sum last position + lest header width - w = p[p.length-1] + h[h.length-1]; - - // !Set tableWidth - w && ( tableWidth = w ); - - // Ajust spacing - // tableWidth = tableWidth - (h.length * columnSpacing); - - this.logg('columnSizes', h); - this.logg('columnPositions', p); - - }; - - calcColumnSizes(); - - // Header - - const addHeader = () => { - - // Allow the user to override style for headers - prepareHeader(); - - // calc header height - if(this.headerHeight === 0){ - this.headerHeight = computeRowHeight(table.headers, true); - this.logg(this.headerHeight, 'headers'); - } - - // calc first table line when init table - if(firstLineHeight === 0){ - if(table.datas.length > 0){ - firstLineHeight = computeRowHeight(table.datas[0], true); - this.logg(firstLineHeight, 'datas'); - } - if(table.rows.length > 0){ - firstLineHeight = computeRowHeight(table.rows[0], true); - this.logg(firstLineHeight, 'rows'); - } - } - - // 24.1 is height calc title + subtitle - titleHeight = !lockAddTitles ? 24.1 : 0; - // calc if header + first line fit on last page - const calc = startY + titleHeight + firstLineHeight + this.headerHeight + safelyMarginBottom// * 1.3; - - // content is big text (crazy!) - if(firstLineHeight > maxY) { - // lockAddHeader = true; - lockAddPage = true; - this.logg('CRAZY! This a big text on cell'); - } else if(calc > maxY) { // && !lockAddPage - // lockAddHeader = false; - lockAddPage = false; - onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - return; - } - - // if has title - if(lockAddTitles === false) { - - // create title and subtitle - createTitle( title, 12, 1 ); - createTitle( subtitle, 9, 0.7 ); - - // add space after title - if( title || subtitle ){ - startY += 3; - }; - - } - - // Allow the user to override style for headers - prepareHeader(); - - lockAddTitles = true; - - // this options is trial - if(options.absolutePosition === true){ - lastPositionX = options.x || startX || this.x; // x position head - startY = options.y || startY || this.y; // x position head - } else { - lastPositionX = startX; // x position head - } - - // Check to have enough room for header and first rows. default 3 - // if (startY + 2 * this.headerHeight >= maxY) this.emitter.emit('addPage'); //this.addPage(); - - if(!options.hideHeader && table.headers.length > 0) { - - // simple header - if(typeof table.headers[0] === 'string') { - - // // background header - // const rectRow = { - // x: startX, - // y: startY - columnSpacing - (rowDistance * 2), - // width: columnWidth, - // height: this.headerHeight + columnSpacing, - // }; - - // // add background - // this.addBackground(rectRow); - - // print headers - table.headers.forEach((header, i) => { - - // background header - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: columnSizes[i], - height: this.headerHeight + columnSpacing, - }; - - // add background - this.addBackground(rectCell); - - // cell padding - cellPadding = prepareCellPadding(options.padding || 0); - - // write - this.text(header, - lastPositionX + (cellPadding.left), - startY, { - width: Number(columnSizes[i]) - (cellPadding.left + cellPadding.right), - align: 'left', - }); - - lastPositionX += columnSizes[i] >> 0; - - }); - - }else{ - - // Print all headers - table.headers.forEach( (dataHeader, i) => { - - let {label, width, renderer, align, headerColor, headerOpacity, headerAlign, padding} = dataHeader; - // check defination - width = width || columnSizes[i]; - align = headerAlign || align || 'left'; - // force number - width = width >> 0; - - // register renderer function - if(renderer && typeof renderer === 'string') { - table.headers[i].renderer = fEval(renderer); - } - - // # Rotation - // var doTransform = function (x, y, angle) { - // var rads = angle / 180 * Math.PI; - // var newX = x * Math.cos(rads) + y * Math.sin(rads); - // var newY = y * Math.cos(rads) - x * Math.sin(rads); - - // return { - // x: newX, - // y: newY, - // rads: rads, - // angle: angle - // }; - // }; - // } - // this.save(); // rotation - // this.rotate(90, {origin: [lastPositionX, startY]}); - // width = 50; - - // background header - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: width, - height: this.headerHeight + columnSpacing, - }; - - // add background - this.addBackground(rectCell, headerColor, headerOpacity); - - // cell padding - cellPadding = prepareCellPadding(padding || options.padding || 0); - - // write - this.text(label, - lastPositionX + (cellPadding.left), - startY, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }) - - lastPositionX += width; - // this.restore(); // rotation - - }); - - } - - // set style - prepareRowOptions(table.headers); - - } - - if(!options.hideHeader) { - // Refresh the y coordinate of the bottom of the headers row - rowBottomY = Math.max(startY + computeRowHeight(table.headers, true), rowBottomY); - // Separation line between headers and rows - separationsRow('header', startX, rowBottomY); - } else { - rowBottomY = startY; - } - - }; - - // End header - addHeader(); - - // Datas - table.datas.forEach((row, i) => { - - this.datasIndex = i; - const rowHeight = computeRowHeight(row, false); - this.logg(rowHeight); - - // Switch to next page if we cannot go any further because the space is over. - // For safety, consider 3 rows margin instead of just one - // if (startY + 2 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - // else this.emitter.emit('addPage'); //this.addPage(); - console.log('plugin datas', this.y, this.y + safelyMarginBottom + rowHeight) - if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // calc position - startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - - // unlock add page function - lockAddPage = false; - - const rectRow = { - x: startX, - y: startY - columnSpacing - (rowDistance * 2), - width: tableWidth - startX, - height: rowHeight + columnSpacing, - }; - - // add background row - prepareRowBackground(row, rectRow); - - lastPositionX = startX; - - // Print all cells of the current row - table.headers.forEach(( dataHeader, index) => { - - let {property, width, renderer, align, valign, padding} = dataHeader; - - // check defination - width = width || columnWidth; - align = align || 'left'; - - // cell padding - cellPadding = prepareCellPadding(padding || options.padding || 0); - - const rectCell = { - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: width, - height: rowHeight + columnSpacing, - } - - // allow the user to override style for rows - prepareRowOptions(row); - prepareRow(row, index, i, rectRow, rectCell,); - - let text = row[property]; - - // cell object - if(typeof text === 'object' ){ - - text = String(text.label); // get label - // row[property].hasOwnProperty('options') && prepareRowOptions(row[property]); // set style - - // options if text cell is object - if( row[property].hasOwnProperty('options') ){ - - // set font style - prepareRowOptions(row[property]); - prepareRowBackground(row[property], rectCell); - - } - - } else { - - // style column by header - prepareRowBackground(table.headers[index], rectCell); - - } - - // bold - if( String(text).indexOf('bold:') === 0 ){ - this.font('Helvetica-Bold'); - text = text.replace('bold:',''); - } - - // size - if( String(text).indexOf('size') === 0 ){ - let size = String(text).substr(4,2).replace(':','').replace('+','') >> 0; - this.fontSize( size < 7 ? 7 : size ); - text = text.replace(`size${size}:`,''); - } - - // renderer column - // renderer && (text = renderer(text, index, i, row, rectRow, rectCell)) // value, index-column, index-row, row nbhmn - if(typeof renderer === 'function'){ - text = renderer(text, index, i, row, rectRow, rectCell); // value, index-column, index-row, row, doc[this] - } - - // TODO # Experimental - // ------------------------------------------------------------------------------ - // align vertically - let topTextToAlignVertically = 0; - if(valign && valign !== 'top'){ - const heightText = this.heightOfString(text, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }); - // line height, spacing hehight, cell and text diference - topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; - } - // ------------------------------------------------------------------------------ - - this.text(text, - lastPositionX + (cellPadding.left), - startY + topTextToAlignVertically, { - width: width - (cellPadding.left + cellPadding.right), - align: align, - }); - - lastPositionX += width; - - // set style - // Maybe REMOVE ??? - prepareRowOptions(row); - prepareRow(row, index, i, rectRow, rectCell); - - }); - - // Refresh the y coordinate of the bottom of this row - rowBottomY = Math.max(startY + rowHeight, rowBottomY); - - // console.log(this.page.height, rowBottomY, this.y); - // text is so big as page (crazy!) - if(rowBottomY > this.page.height) { - rowBottomY = this.y + columnSpacing + (rowDistance * 2); - } - - // Separation line between rows - separationsRow('horizontal', startX, rowBottomY); - - // review this code - if( row.hasOwnProperty('options') ){ - if( row.options.hasOwnProperty('separation') ){ - // Separation line between rows - separationsRow('horizontal',startX, rowBottomY, 1, 1); - } - } - - }); - // End datas - - // Rows - table.rows.forEach((row, i) => { - - this.rowsIndex = i; - const rowHeight = computeRowHeight(row, false); - this.logg(rowHeight); - - // Switch to next page if we cannot go any further because the space is over. - // For safety, consider 3 rows margin instead of just one - // if (startY + 3 * rowHeight < maxY) startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - // else this.emitter.emit('addPage'); //this.addPage(); - if(options.useSafelyMarginBottom && this.y + safelyMarginBottom + rowHeight >= maxY && !lockAddPage) onFirePageAdded(); // this.emitter.emit('addPage'); //this.addPage(); - - // calc position - startY = rowBottomY + columnSpacing + rowDistance; // 0.5 is spacing rows - - // unlock add page function - lockAddPage = false; - - const rectRow = { - x: columnPositions[0], - // x: startX, - y: startY - columnSpacing - (rowDistance * 2), - width: tableWidth - startX, - height: rowHeight + columnSpacing, - } - - // add background - // doc.addBackground(rectRow); - - lastPositionX = startX; - - row.forEach((cell, index) => { - - let align = 'left'; - let valign = undefined; - - const rectCell = { - // x: columnPositions[index], - x: lastPositionX, - y: startY - columnSpacing - (rowDistance * 2), - width: columnSizes[index], - height: rowHeight + columnSpacing, - } - - prepareRowBackground(table.headers[index], rectCell); - - // Allow the user to override style for rows - prepareRow(row, index, i, rectRow, rectCell); - - if(typeof table.headers[index] === 'object') { - // renderer column - table.headers[index].renderer && (cell = table.headers[index].renderer(cell, index, i, row, rectRow, rectCell, this)); // text-cell, index-column, index-line, row, doc[this] - // align - table.headers[index].align && (align = table.headers[index].align); - table.headers[index].valign && (valign = table.headers[index].valign); - } - - // cell padding - cellPadding = prepareCellPadding(table.headers[index].padding || options.padding || 0); - - // TODO # Experimental - // ------------------------------------------------------------------------------ - // align vertically - let topTextToAlignVertically = 0; - if(valign && valign !== 'top'){ - const heightText = this.heightOfString(cell, { - width: columnSizes[index] - (cellPadding.left + cellPadding.right), - align: align, - }); - // line height, spacing hehight, cell and text diference - topTextToAlignVertically = rowDistance - columnSpacing + (rectCell.height - heightText) / 2; - } - // ------------------------------------------------------------------------------ - - this.text(cell, - lastPositionX + (cellPadding.left), - startY + topTextToAlignVertically, { - width: columnSizes[index] - (cellPadding.left + cellPadding.right), - align: align, - }); - - lastPositionX += columnSizes[index]; - - }); - - // Refresh the y coordinate of the bottom of this row - rowBottomY = Math.max(startY + rowHeight, rowBottomY); - - // console.log(this.page.height, rowBottomY, this.y); - // text is so big as page (crazy!) - if(rowBottomY > this.page.height) { - rowBottomY = this.y + columnSpacing + (rowDistance * 2); - } - - // Separation line between rows - separationsRow('horizontal', startX, rowBottomY); - - }); - // End rows - - // update position - this.x = startX; - this.y = rowBottomY; // position y final; - this.moveDown(); // break - - // add fire - this.off("pageAdded", onFirePageAdded); - - // callback - typeof callback === 'function' && callback(this); - - // nice :) - resolve(); - - } catch (error) { - - // error - reject(error); - - } - - }); - } - - /** - * tables - * @param {Object} tables - * @returns - */ - async tables(tables, callback) { - return new Promise(async (resolve, reject) => { - try { - - if(Array.isArray(tables) === false) - { - resolve(); - return; - } - - const len = tables.length; - for(let i; i < len; i++) - { - await this.table(tables[i], tables[i].options || {}); - } - - // if tables is Array - // Array.isArray(tables) ? - // // for each on Array - // tables.forEach( async table => await this.table( table, table.options || {} ) ) : - // // else is tables is a unique table object - // ( typeof tables === 'object' ? this.table( tables, tables.options || {} ) : null ) ; - // // callback - typeof callback === 'function' && callback(this); - // // donw! - resolve(); - } - catch(error) - { - reject(error); - } - - }); - } - -} - -module.exports = PDFDocumentWithTables; -module.exports.default = PDFDocumentWithTables;