Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
```
- Fixed extra blank page when using headerRows, dontBreakRows and cell pageBreak together
- Fixed rendering of an invalid color name - previously it used the last valid color; now it defaults to black
- Added dynamic `pageMargins`

## 0.3.7 - 2026-03-17

Expand Down
68 changes: 68 additions & 0 deletions examples/dynamicPageMargins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*eslint no-unused-vars: ["error", {"args": "none"}]*/

var pdfmake = require('../js/index'); // only during development, otherwise use the following line
//var pdfmake = require('pdfmake');

var Roboto = require('../fonts/Roboto');
pdfmake.addFonts(Roboto);

pdfmake.setUrlAccessPolicy((url) => {
// this can be used to restrict allowed domains
return url.startsWith('https://');
});

var loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ';

var docDefinition = {
// Stable usage: page-local rules based on currentPage do not feed pagination
// back into the callback, so layout converges naturally.
pageMargins: function(currentPage, pageCount, pageSize) {
return {
left: (currentPage % 2 === 0) ? 80 : 40,
top: 40,
right: (currentPage % 2 === 0) ? 40 : 80,
bottom: 40
};
},
content: [
{ text: loremIpsum.repeat(42) },
'',
'Table:',
{
table: {
body: [
[{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader' }, { text: 'Header 3', style: 'tableHeader' }],
[
loremIpsum.repeat(4),
loremIpsum.repeat(4),
loremIpsum.repeat(4),
]
]
}
},
'',
'Table width headerRows:',
{
table: {
headerRows: 1,
body: [
[{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader' }, { text: 'Header 3', style: 'tableHeader' }],
[
loremIpsum.repeat(6),
loremIpsum.repeat(6),
loremIpsum.repeat(6),
]
]
}
}
]
};

var now = new Date();

var pdf = pdfmake.createPdf(docDefinition);
pdf.write('pdfs/dynamicPageMargins.pdf').then(() => {
console.log(new Date() - now);
}, err => {
console.error(err);
});
44 changes: 44 additions & 0 deletions examples/dynamicPageMarginsModuloParadox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
var pdfmake = require('../js/index'); // only during development, otherwise use the following line
//var pdfmake = require('pdfmake');

var Roboto = require('../fonts/Roboto');
pdfmake.addFonts(Roboto);

var marginCalls = [];
var lines = [];

for (var i = 0; i < 3; i++) {
lines.push('Line ' + i + ' with some text to fill the page and change pagination.');
}

var docDefinition = {
pageSize: 'A7',
content: lines,
pageMargins: function (currentPage, pageCount) {
marginCalls.push({ currentPage: currentPage, pageCount: pageCount });

if (!pageCount) {
return { left: 40, top: 40, right: 40, bottom: 40 };
}

// An intentionally paradoxical case with pageCount = 1 every page
// satisfies currentPage % pageCount === 0, so the callback can flip the
// document between one-page and two-page layouts across passes.
if (currentPage % pageCount === 0) {
return { left: 40, bottom: 40, right: 40, top: 140 };
}

return { left: 40, bottom: 40, right: 40, top: 40 };
}
};

var now = new Date();

var pdf = pdfmake.createPdf(docDefinition);
pdf.write('pdfs/dynamicPageMarginsModuloParadox.pdf').then(function () {
console.log(new Date() - now);
console.log('pageMargins callback calls:', marginCalls.length);
console.log(JSON.stringify(marginCalls, null, 2));
}, function (err) {
console.error(err);
});
39 changes: 39 additions & 0 deletions examples/dynamicPageMarginsPageCountParadox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
var pdfmake = require('../js/index'); // only during development, otherwise use the following line
//var pdfmake = require('pdfmake');

var Roboto = require('../fonts/Roboto');
pdfmake.addFonts(Roboto);

var marginCalls = [];
var lines = [];

for (var i = 0; i < 5; i++) {
lines.push('Line ' + i + ' with some text to fill the page and change pagination.');
}

var docDefinition = {
pageSize: 'A7',
content: lines,
pageMargins: function (currentPage, pageCount) {
marginCalls.push({ currentPage: currentPage, pageCount: pageCount });

// This is intentionally paradoxical: changing margins based on the assumed
// total page count can change pagination, which changes pageCount again.
if (pageCount % 2 === 1) {
return { left: 40, bottom: 40, right: 40, top: 140 };
}

return { left: 40, bottom: 40, right: 40, top: 40 };
}
};

var now = new Date();

var pdf = pdfmake.createPdf(docDefinition);
pdf.write('pdfs/dynamicPageMarginsPageCountParadox.pdf').then(function () {
console.log(new Date() - now);
console.log('pageMargins callback calls:', marginCalls.length);
console.log(JSON.stringify(marginCalls, null, 2));
}, function (err) {
console.error(err);
});
Binary file added examples/pdfs/dynamicPageMargins.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
82 changes: 66 additions & 16 deletions src/DocumentContext.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isString } from './helpers/variableType';
import { EventEmitter } from 'events';
import { normalizePageMargin } from './PageSize';

/**
* A store for current x, y positions and available width/height.
Expand All @@ -10,6 +11,8 @@ class DocumentContext extends EventEmitter {
super();
this.pages = [];
this.pageMargins = undefined;
this.pageCount = 0;
this.pageMarginFunctionUsed = false;
this.x = undefined;
this.availableWidth = undefined;
this.availableHeight = undefined;
Expand Down Expand Up @@ -328,8 +331,8 @@ class DocumentContext extends EventEmitter {
return;
}

let pageTopY = this.pageMargins.top;
let pageInnerHeight = this.getCurrentPage().pageSize.height - this.pageMargins.top - this.pageMargins.bottom;
let pageTopY = this.getCurrentPage().pageMargins.top;
let pageInnerHeight = this.getCurrentPage().pageSize.height - this.getCurrentPage().pageMargins.top - this.getCurrentPage().pageMargins.bottom;

// When moving to new page, start at first column.
// Reset width to FIRST column width, not last column from previous page.
Expand All @@ -342,9 +345,9 @@ class DocumentContext extends EventEmitter {

// Reset X to start of first column (left margin)
if (this.marginXTopParent) {
this.x = this.pageMargins.left + this.marginXTopParent[0];
this.x = this.getCurrentPage().pageMargins.left + this.marginXTopParent[0];
} else {
this.x = this.pageMargins.left;
this.x = this.getCurrentPage().pageMargins.left;
}
this.availableWidth = firstColumnWidth;
this.lastColumnWidth = firstColumnWidth;
Expand All @@ -370,6 +373,43 @@ class DocumentContext extends EventEmitter {
}
}

restoreColumnStateAfterPageBreak(previousColumnState) {
if (!previousColumnState || this.snapshots.length === 0) {
return;
}

let currentPage = this.getCurrentPage();
if (!currentPage || !currentPage.pageMargins) {
return;
}

// Column snapshots store absolute X coordinates from the previous page.
// When margins change per page, we must preserve the offset from the left
// margin instead of reusing the old absolute X.
let previousPageMargins = previousColumnState.pageMargins || { left: 0 };
let translateX = x => currentPage.pageMargins.left + (x - previousPageMargins.left);
let currentState = {
x: translateX(previousColumnState.x),
y: this.y,
page: this.page,
availableHeight: this.availableHeight,
availableWidth: previousColumnState.availableWidth
};

this.x = currentState.x;
this.availableWidth = previousColumnState.availableWidth;

for (let i = 0; i < this.snapshots.length; i++) {
let snapshot = this.snapshots[i];
snapshot.x = translateX(snapshot.x);
snapshot.y = this.y;
snapshot.page = this.page;
snapshot.availableHeight = this.availableHeight;

snapshot.bottomMost = bottomMostContext(currentState, snapshot.bottomMost || currentState);
}
}

addMargin(left, right) {
this.x += left;
this.availableWidth -= left + (right || 0);
Expand All @@ -383,10 +423,10 @@ class DocumentContext extends EventEmitter {
}

initializePage() {
this.y = this.pageMargins.top;
this.availableHeight = this.getCurrentPage().pageSize.height - this.pageMargins.top - this.pageMargins.bottom;
this.y = this.getCurrentPage().pageMargins.top;
this.availableHeight = this.getCurrentPage().pageSize.height - this.getCurrentPage().pageMargins.top - this.getCurrentPage().pageMargins.bottom;
const { pageCtx, isSnapshot } = this.pageSnapshot();
pageCtx.availableWidth = this.getCurrentPage().pageSize.width - this.pageMargins.left - this.pageMargins.right;
pageCtx.availableWidth = this.getCurrentPage().pageSize.width - this.getCurrentPage().pageMargins.left - this.getCurrentPage().pageMargins.right;
if (isSnapshot && this.marginXTopParent) {
pageCtx.availableWidth -= this.marginXTopParent[0];
pageCtx.availableWidth -= this.marginXTopParent[1];
Expand All @@ -404,11 +444,11 @@ class DocumentContext extends EventEmitter {
moveTo(x, y) {
if (x !== undefined && x !== null) {
this.x = x;
this.availableWidth = this.getCurrentPage().pageSize.width - this.x - this.pageMargins.right;
this.availableWidth = this.getCurrentPage().pageSize.width - this.x - this.getCurrentPage().pageMargins.right;
}
if (y !== undefined && y !== null) {
this.y = y;
this.availableHeight = this.getCurrentPage().pageSize.height - this.y - this.pageMargins.bottom;
this.availableHeight = this.getCurrentPage().pageSize.height - this.y - this.getCurrentPage().pageMargins.bottom;
}
}

Expand Down Expand Up @@ -485,11 +525,21 @@ class DocumentContext extends EventEmitter {
addPage(pageSize, pageMargin = null, customProperties = {}) {
if (pageMargin !== null) {
this.pageMargins = pageMargin;
this.x = pageMargin.left;
this.availableWidth = pageSize.width - pageMargin.left - pageMargin.right;
}

let page = { items: [], pageSize: pageSize, pageMargins: this.pageMargins, customProperties: customProperties };
let currentMargin = pageMargin || this.pageMargins;

if (typeof currentMargin === 'function') {
this.pageMarginFunctionUsed = true;
currentMargin = normalizePageMargin(currentMargin(this.pages.length + 1, this.pageCount, pageSize));
}

if (currentMargin !== undefined && currentMargin !== null) {
this.x = currentMargin.left;
this.availableWidth = pageSize.width - currentMargin.left - currentMargin.right;
}

let page = { items: [], pageSize: pageSize, pageMargins: currentMargin, customProperties: customProperties };
this.pages.push(page);
this.backgroundLength.push(0);
this.page = this.pages.length - 1;
Expand All @@ -510,8 +560,8 @@ class DocumentContext extends EventEmitter {

getCurrentPosition() {
let pageSize = this.getCurrentPage().pageSize;
let innerHeight = pageSize.height - this.pageMargins.top - this.pageMargins.bottom;
let innerWidth = pageSize.width - this.pageMargins.left - this.pageMargins.right;
let innerHeight = pageSize.height - this.getCurrentPage().pageMargins.top - this.getCurrentPage().pageMargins.bottom;
let innerWidth = pageSize.width - this.getCurrentPage().pageMargins.left - this.getCurrentPage().pageMargins.right;

return {
pageNumber: this.page + 1,
Expand All @@ -520,8 +570,8 @@ class DocumentContext extends EventEmitter {
pageInnerWidth: innerWidth,
left: this.x,
top: this.y,
verticalRatio: ((this.y - this.pageMargins.top) / innerHeight),
horizontalRatio: ((this.x - this.pageMargins.left) / innerWidth)
verticalRatio: ((this.y - this.getCurrentPage().pageMargins.top) / innerHeight),
horizontalRatio: ((this.x - this.getCurrentPage().pageMargins.left) / innerWidth)
};
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/ElementWriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ class ElementWriter extends EventEmitter {
if (isNumber(contextOrWidth)) {
let width = contextOrWidth;
contextOrWidth = new DocumentContext();
contextOrWidth.pageMargins = this.context().pageMargins;
contextOrWidth.pageCount = this.context().pageCount;
contextOrWidth.addPage({ width: width, height: height }, { left: 0, right: 0, top: 0, bottom: 0 });
}

Expand Down
Loading
Loading