Helps to draw information in simple tables using pdfkit.
v0.2.00 — full TypeScript rewrite, ESM
importsupport and pdfkit dependency injection,
Examples (open)
- HTTP server — JS | (streamed response)
- Basic table — JS | PDF
- Colors — JS | PDF
- JSON +
table.json— JS | PDF - All scenarios — JS | PDF
- All features — JS | PDF
- Pages in row — JS | PDF
- Images — JS | PDF
- Headers — JS | PDF
- RTL (right-to-left) — JS | PDF
- Landscape — JS | PDF
- Many lines — JS | PDF
yarn add pdfkit-tablenpm install pdfkit-tableAll three styles work out of the box — no bundler configuration needed:
// ESM / Node ≥ 12
import PDFDocument from 'pdfkit-table';
import { PDFDocumentWithTables, createPdfDocumentWithTables } from 'pdfkit-table';
// TypeScript
import PDFDocument, { type Table, type TableOptions } from 'pdfkit-table';
// CommonJS
const PDFDocument = require('pdfkit-table');
const { PDFDocumentWithTables, createPdfDocumentWithTables } = require('pdfkit-table');Use createPdfDocumentWithTables when you want to plug in your pdfkit package (different semver, fork, patched build, or single shared copy with the rest of the app). The constructor must stay compatible with pdfkit’s PDFDocument (same methods this library calls on super, drawing API, fonts, etc.).
const fs = require('fs');
const pdfkit = require('pdfkit'); // resolved from your project / fork
const { createPdfDocumentWithTables } = require('pdfkit-table');
const PDFDocument = createPdfDocumentWithTables(pdfkit);
const doc = new PDFDocument({ margin: 30, size: 'A4' });
doc.pipe(fs.createWriteStream('./document.pdf'));
(async () => {
await doc.table({ headers: ['Column'], rows: [['value']] }, {});
doc.end();
})();import fs from 'fs';
import pdfkit from 'pdfkit';
import { createPdfDocumentWithTables } from 'pdfkit-table';
const PDFDocument = createPdfDocumentWithTables(pdfkit);
const doc = new PDFDocument({ margin: 30, size: 'A4' });
doc.pipe(fs.createWriteStream('./document.pdf'));
void (async () => {
await doc.table({ headers: ['Column'], rows: [['value']] }, {});
doc.end();
})();Minimal flow: create a document, await doc.table(...) (tables are asynchronous), then doc.end().
const fs = require('fs');
const PDFDocument = require('pdfkit-table');
const doc = new PDFDocument({ margin: 30, size: 'A4' });
doc.pipe(fs.createWriteStream('./document.pdf'));
(async () => {
const table = {
title: '',
headers: [],
data: [], // keyed rows ({ property } per header)
rows: [], // or simple string[][] when headers are strings
};
await doc.table(table, { /* TableOptions — width, prepareRow, … */ });
// Express: pipe once — doc.pipe(res);
doc.end(); // closes the stream after all awaited tables resolve
})();The Examples section at the top lists every script and PDF under example/. Below are the same patterns in short form for documentation.
Pipe the PDFKit stream once (to res or to fs). Avoid doc.pipe(fs) and doc.pipe(res) on the same document.
app.get('/create-pdf', async (req, res) => {
const PDFDocument = require('pdfkit-table');
res.setHeader('Content-Type', 'application/pdf');
const doc = new PDFDocument({ margin: 30, size: 'A4' });
doc.pipe(res);
const table = {
headers: ['Country', 'Conversion rate'],
rows: [['Switzerland', '12%']],
};
await doc.table(table, { width: 300 });
doc.end();
});See also example/document-00-server.js.
;(async () => {
const table = {
title: 'Title',
subtitle: 'Subtitle',
headers: ['Country', 'Conversion rate', 'Trend'],
rows: [
['Switzerland', '12%', '+1.12%'],
['France', '67%', '-0.98%'],
['England', '33%', '+4.44%'],
],
};
await doc.table(table, { width: 300 });
// …or explicit column widths (pt): { columnsSize: [200, 100, 100] }
doc.end();
})();;(async () => {
const table = {
title: 'Title',
subtitle: 'Subtitle',
headers: [
{ label: 'Name', property: 'name', width: 60, renderer: null },
{ label: 'Description', property: 'description', width: 150, renderer: null },
{ label: 'Price 1', property: 'price1', width: 100, renderer: null },
{ label: 'Price 2', property: 'price2', width: 100, renderer: null },
{ label: 'Price 3', property: 'price3', width: 80, renderer: null },
{
label: 'Price 4',
property: 'price4',
width: 43,
renderer: (value, indexColumn, indexRow, row, rectRow, rectCell) =>
`U$ ${Number(value).toFixed(2)}`,
},
],
data: [
{
name: 'Name 1',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean mattis ante in laoreet egestas. ',
price1: '$1',
price3: '$ 3',
price2: '$2',
price4: '4',
},
{
options: { fontSize: 10, separation: true },
name: 'bold:Name 2',
description: 'bold:Lorem ipsum dolor.',
price1: 'bold:$1',
price3: { label: 'PRICE $3', options: { fontSize: 12 } },
price2: '$2',
price4: '4',
},
],
rows: [
[
'Apple',
'Nullam ut facilisis mi. Nunc dignissim ex ac vulputate facilisis.',
'$ 105,99',
'$ 105,99',
'$ 105,99',
'105.99',
],
],
};
await doc.table(table, {
prepareHeader: () => doc.font('Helvetica-Bold').fontSize(8),
prepareRow: (row, indexColumn, indexRow, rectRow, rectCell) => {
doc.font('Helvetica').fontSize(8);
if (indexColumn === 0) doc.addBackground(rectRow, 'blue', 0.15);
},
});
doc.end();
})();String renderers belong on headers[].renderer when you need serialized JSON (@deprecated — prefer real functions).
;(async () => {
const tableJson = JSON.stringify({
headers: [
{ label: 'Name', property: 'name', width: 100 },
{ label: 'Age', property: 'age', width: 100 },
{
label: 'Year',
property: 'year',
width: 100,
renderer:
'function(value, indexColumn, indexRow){ return value + "(" + (1 + indexRow) + ")"; }',
},
],
data: [
{ name: 'bold:Name 1', age: 'Age 1', year: 'Year 1' },
{ name: 'Name 2', age: 'Age 2', year: 'Year 2' },
{ name: 'Name 3', age: 'Age 3', year: 'Year 3' },
],
rows: [['Name 4', 'Age 4', 'Year 4']],
options: { width: 300 },
});
await doc.table(tableJson); // parses JSON; merges embedded `options`
doc.end();
})();See example/table.json.
;(async () => {
const json = require('./table.json');
if (Array.isArray(json)) {
await doc.tables(json);
} else {
await doc.table(json, json.options ?? {});
}
doc.end();
})();Array.<object>|JSON- headers
Array.<object>|Array.[]- label
String - property
String - width
Number - align
String - valign
String - headerColor
String - headerOpacity
Number - headerAlign
String - columnColor or
backgroundColor:String - columnOpacity or
backgroundOpacity:Number - padding
Number|Array|Object - renderer
Functionfunction( value, indexColumn, indexRow, row, rectRow, rectCell ) { return value }
- label
- data
Array.<object> datasArray.<object>(deprecated — usedata)- rows
Array.[] - title
String|Object - subtitle
String|Object - options
Object
- headers
| Properties | Type | Default | Description |
|---|---|---|---|
| label | String |
undefined | description |
| property | String |
undefined | id |
| width | Number |
undefined | width of column |
| align | String |
left | alignment |
| valign | String |
undefined | vertical alignment. ex: valign: "center" |
| headerColor | String |
grey or #BEBEBE | color of header |
| headerOpacity | Number |
0.5 | opacity of header |
| headerAlign | String |
left | only header |
| columnColor or |
String |
undefined | color of column |
| columnOpacity or |
Number |
undefined | opacity of column |
| padding | `Number | Array | Object` |
| renderer | Function |
Function | function( value, indexColumn, indexRow, row, rectRow, rectCell ) { return value } |
const table = {
// simple headers only with ROWS (not DATA)
headers: ['Name', 'Age'],
// simple content
rows: [
['Jack', '32'], // row 1
['Maria', '30'], // row 2
]
};const table = {
// complex headers work with ROWS and DATA
headers: [
{ label:"Name", property: 'name', width: 100, renderer: null },
{ label:"Age", property: 'age', width: 100, renderer: (value) => `U$ ${Number(value).toFixed(1)}` },
],
// complex content
data: [
{ name: 'bold:Jack', age: 32, },
// age is object value with style options
{ name: 'Maria', age: { label: 30 , options: { fontSize: 12 }}, },
],
// simple content (works fine!)
rows: [
['Jack', '32'], // row 1
['Maria', '30'], // row 2
]
};| Property | Type | Default | Description |
|---|---|---|---|
| title | `String | Object` | undefined |
| subtitle | `String | Object` | undefined |
| width | Number |
undefined | total table width |
| x | `Number | null` | undefined |
| y | Number |
undefined | y position (top) |
| divider | Object |
— | divider line config { header, horizontal, vertical } |
| columnsSize | Array |
[] |
column widths (simple tables) |
| columnSpacing | Number |
3 |
vertical space between rows |
| padding | `Number | Array | Object` |
| addPage | Boolean |
false |
start table on a fresh page |
| hideHeader | Boolean |
false |
hide the header row |
| minRowHeight | Number |
0 |
minimum row height in points |
| useSafelyMarginBottom | Boolean |
true |
enable proactive page-break before rows that do not fit |
| pageBreakThreshold | Number (0–1) |
0.8 |
fraction of page height below which a row triggers a proactive page break. Rows taller than pageContentHeight × threshold render in-place without an empty gap. Default 0.8 means only rows that fill < 80 % of the page are moved to a new page. |
| endOfPageThreshold | Number (0–1) |
— | fraction of usable page height defining "near the bottom". A proactive break fires when remaining space ≤ this fraction AND the row fits within pageBreakThreshold. Default: page bottom margin |
| keepRowsTogether | Boolean |
false |
when true, every row starts at the current cursor — no proactive page breaks. Ideal for tables where every cell contains multi-page text. |
| absolutePosition | Boolean |
false |
use absolute x / y coordinates |
| prepareHeader | Function |
— | (this: PDFDoc) => void — called before rendering the header row |
| prepareRow | Function |
— | (row, indexColumn, indexRow, rectRow, rectCell) => void — called before each cell |
const options = {
title: "Title", // or { label: 'Title', fontSize: 18, color: 'blue', fontFamily: "./fonts/type.ttf" }
subtitle: "Subtitle",
width: 500, // A4 portrait ≈ 595 pt wide
x: 0, // pass null or -1 to reset to left margin
y: 0,
divider: {
header: { disabled: false, width: 2, opacity: 1 },
horizontal: { disabled: false, width: 0.5, opacity: 0.5 },
},
padding: 5, // or [top, right, bottom, left] like CSS
columnSpacing: 5,
hideHeader: false,
minRowHeight: 0,
prepareHeader: () => doc.font("Helvetica-Bold").fontSize(8),
prepareRow: (row, indexColumn, indexRow, rectRow, rectCell) =>
doc.font("Helvetica").fontSize(8),
}// Option A — pageBreakThreshold
// Only move rows to a new page if they fit in < 60 % of the page.
// Rows taller than 60 % start in-place and flow naturally across pages.
await doc.table(table, {
pageBreakThreshold: 0.6,
prepareHeader: () => doc.font('Helvetica-Bold').fontSize(8),
prepareRow: () => doc.font('Helvetica').fontSize(8),
});
// Option B — keepRowsTogether
// Never insert a proactive page break — every row starts where the cursor is.
// Best for tables where every cell contains long multi-page text.
await doc.table(table, {
keepRowsTogether: true,
prepareHeader: () => doc.font('Helvetica-Bold').fontSize(8),
prepareRow: () => doc.font('Helvetica').fontSize(8),
});pageBreakThreshold |
Effect |
|---|---|
0.8 (default) |
Only rows shorter than 80 % of the page are moved proactively — tall rows stay in-place and overflow naturally. Equivalent to "only addPage() breaks the page for big rows." |
1.0 |
Old behaviour — every row that doesn't fit in remaining space gets a page break, regardless of height. |
0.6 |
Only move if row < 60 % of page height — tall rows flow in-place |
0.0 |
Never move any row (same as keepRowsTogether: true) |
A chainable helper method available on any PDFDocumentWithTables instance.
Call it before a section title, heading, or doc.table() to ensure there
is enough room on the current page. If the remaining vertical space is less
than minHeight, a new page is added automatically.
| Argument | Type | Default | Meaning |
|---|---|---|---|
| (none) | — | 10 % of usable height | add a page if less than 10 % remains |
0 < n ≤ 1 |
Number (fraction) |
— | treat as percentage of usable page height |
n > 1 |
Number (points) |
— | minimum absolute space required (pt) |
Returns this so calls can be chained fluently.
// Default — add a page if less than 10 % of usable height remains
doc.checkPageBreak();
// At least 80 pt must remain, otherwise add a new page
doc.checkPageBreak(80);
// At least 15 % of the usable page height must remain
doc.checkPageBreak(0.15);
// Typical chained usage — keeps a title and its table together
doc
.checkPageBreak(0.2) // ensure 20 % space before writing the title
.fontSize(11)
.font('Helvetica-Bold')
.text('Section Title')
.font('Helvetica')
.fontSize(9)
.moveDown(0.3);
await doc.table(myTable, opts);- separation
{Boolean} - color
{String} - columnColor
{String} - columnOpacity
{Number} - backgroundColor
{String}(deprecated — usecolumnColor) - backgroundOpacity
{Number}(deprecated — usecolumnOpacity) - background
{Object}{ color, opacity }(deprecated — usecolumnColor/columnOpacity) - fontSize
{Number} - fontFamily
{String}
data: [
// options row
{ name: 'Jack', options: { fontSize: 10, fontFamily: 'Courier-Bold', separation: true } },
]- String
- bold:
- 'bold:Jack'
- size{n}:
- 'size11:Jack'
- 'size20:Jack'
- bold:
data: [
// bold
{ name: 'bold:Jack' },
// size{n}
{ name: 'size20:Maria' },
{ name: 'size8:Will' },
// normal
{ name: 'San' },
]- fontSize
{Number} - fontFamily
{String} - color
{String}
data: [
// options cell — value is { label, options }
{
name: { label: 'Jack', options: { fontSize: 10, fontFamily: 'Courier-Bold' } },
},
]- Courier
- Courier-Bold
- Courier-Oblique
- Courier-BoldOblique
- Helvetica
- Helvetica-Bold
- Helvetica-Oblique
- Helvetica-BoldOblique
- Symbol
- Times-Roman
- Times-Bold
- Times-Italic
- Times-BoldItalic
- ZapfDingbats
- Suggestions / Issues / Fixes
- striped {Boolean} (corsimcornao)
- colspan - the colspan attribute defines the number of columns a table cell should span.
- sample with database
- margin: marginBottom before, marginTop after
- accept relative column sizes: (null, undefined or '*') JS | PDF
- columnsSize: [50, 300, null],
- columnsSize: [50, 300, undefined, 200],
- columnsSize: [100, '*', 50, null],
- CG memory
- Thanks spanwair-r
doc.image('./chart-large.png', 50, 200, { width: 400 });
// Do not use in repeated images. ex: brand
// Use in large images
doc.purgeImage('./chart-large.png');- RTL
- Thanks moshfeu
options: {
rtl: true, // boolean
}- Fix
- Thanks @mar10-emil
- added render queue onAddPage
- setting renders queue in constructor
- fixed override for addPage
- added debugging logs
- logging for deb debugging
- removed event handler
- added callback
- rendering set in onFirePageAdded
- removed logs and disabled event triggers
- logging for debugging
- testing sections order
- refactored code - Full TypeScript rewrite — source moved to
src/(types.ts,document.ts,index.ts). Ships compileddist/+.d.tsdeclarations. Backward-compatible type aliases preserved (Options,Data,Title,Divider, …). - ESM
importsupport —import PDFDocument from 'pdfkit-table'works in Node.js ESM, TypeScript, and bundlers (Vite, Webpack).package.jsonnow includes an"exports"map. - Dependency injection —
createPdfDocumentWithTables(PDFKit)lets you plug in your ownpdfkitversion or fork. pageBreakThresholdoption (Number 0–1, default0.8) — controls when a tall row is moved to a new page. Rows taller than80 %of the page start in-place and overflow naturally; only shorter rows are moved proactively. Set to1.0to restore the old always-break behaviour.keepRowsTogetheroption (Boolean, defaultfalse) — disables all proactive page breaks; every row starts at the current cursor and overflows naturally.doc.checkPageBreak(minHeight?)— new chainable helper that adds a page when remaining vertical space is less thanminHeight(default: 10 % of usable height; fractions ≤ 1 are treated as percentages; values > 1 as absolute points). Ideal for keeping section titles and their tables on the same page.
pageAddedevent listener —onFirePageAddedwas defined but never registered; repeated header rendering now works correctly on overflow pages.- Font mismatch in height calculation —
computeRowHeightnow appliesprepareRowbeforeheightOfString, so measured height matches rendered height (eliminates gap between text and divider line). - Page-break forced for multi-page rows — rows taller than one full page no longer trigger a forced new page before every row.
- Text style after mid-row page break — continued text on overflow pages no longer inherits the header font / color (
restoreRowStylemechanism). - Text overlap with header on overflow pages — fixed by temporarily raising
page.margins.topafter the header is drawn so PDFKit'sLineWrapper.nextSection()positions continued text below the header. prepareCellPaddingcase 3 — corrected CSS shorthand:[top, right, bottom, left=right](was incorrectly[top, right, bottom, 0]).eval()in renderer — replaced withnew Function()(CSP-safe). String renderers are now@deprecated.String.substr— replaced deprecatedsubstr(4, 2)withslice(4, 6).- Weak types —
anyremoved fromprepareRowOptions,prepareRowBackground,computeRowHeight; replaced withunknown+ runtime guards and aRowHeightInputunion.
- Add options minRowHeight
- Thanks LouiseEH @LouiseEH
options: {
minRowHeight: 30, // pixel
}- Fix first line height
- Thanks José Luis Francisco @JoseLuis21
- Fix header font family or title object
- Thanks @RastaGrzywa
let localType = "./font/Montserrat-Regular.ttf";
const table = {
title: { label: 'Title Object 2', fontSize: 30, color: 'blue', fontFamily: localType },
}- Add options hideHeader
- Thanks Ville @VilleKoo
options: {
hideHeader: true,
}- TypeScript (ts) interface (index.ts)
- Thanks Côte Arthur @CoteArthur
- Avoid a table title appearing alone
- Thanks Alexis Arriola @AlexisArriola
- Problem with long text in cell spreading on several pages
- Thanks Ed @MeMineToMe
- Add Divider Lines on options
options: {
// divider lines
divider: {
header: {disabled: false, width: 0.5, opacity: 0.5},
horizontal: {disabled: true, width: 0.5, opacity: 0.5},
},
}- Thanks Luc Swart @lucswart
- Fix y position.
- Thanks Nabil Tahmidul Karim @nabiltkarim
- Added Promise. table is a Promise();
- Async/Await function
;(async function(){
// create document
const doc = new PDFDocument({ margin: 30, });
// to save on server
doc.pipe(fs.createWriteStream("./my-table.pdf"));
// tables
await doc.table(table, options);
await doc.table(table, options);
await doc.table(table, options);
// done
doc.end();
})();- Added callback.
~~doc.table(table, options, callback)~~;- Added valign on headers options. (ex: valign:"center")
- Added headerAlign, alignment only to header.
headers: [ {label:"Name", property:"name", valign: "center", headerAlign:"right", headerColor:"#FF0000", headerOpacity:0.5 } ]
- Thanks @DPCLive
- Add callback on addBackground function, add .save() and .restore() style.
- Header font color
- Thanks @dev-fema
- Add padding
- Header color and opacity
headers: [ {label:"Name", property:"name", headerColor:"#FF0000", headerOpacity:0.5 } ]
- Thanks Albert Taveras @itsalb3rt
- Align on headers
headers: [ {label:"Name", property:"name", align:"center"} ]
- Thanks Andrea Fucci
- Max size page
- Header height size
- Separate line width
- addHeader() function on all add pages
- Thanks Anders Wasen @QAnders
- addBackground() function to node 8
- Thanks @mehmetunubol
- Add rectCell on renderer
- renderer = ( value, indexColumn, indexRow, row, rectRow, rectCell ) => {}
- Thanks Eduardo Miranda
- Fix paddings and distances
- Remove rowSpacing
- Fix columnSpacing
- Background color on header to colorize column
- headers: [ { label:"Name", property: 'name', backgroundColor: 'red', backgroundOpacity: 0.5 }, { label:"Age", property: 'age', background: { color: 'green', opacity: 0.5 } }, ]
- Background color inside row options data
- data: [ { name:"My Name", age: 20, options: { backgroundColor: 'red', backgroundOpacity: 0.5 } }, { name:"My Name", age: 20, options: { background: { color: 'green', opacity: 0.5 } } }, ]
- Background color inside cell options data
- data: [ { name:{ label: "My Name", age: 20, options: { backgroundColor: 'red', backgroundOpacity: 0.5 } }}, { name:{ label: "My Name", age: 20, options: { background: { color: 'green', opacity: 0.5 } } }}, ]
- addBackground
{Function}- Add background peer line.- doc.addBackground( {x, y, width, height}, fillColor, opacity, callback );
- prepareRow
{Function}- const options = { prepareRow: (row, indexColumn, indexRow, rectRow, rectCell) => { indexColumn === 0 && doc.addBackground(rectRow, 'red', 0.5) } }
- tables
{Function}- Add many tables.- doc.tables([ table0, table1, table2, ... ]);
- addPage
{Boolean}- Add table on new page.- const options = { addPage: true, };
- Fix position x, y of title
- options.x: null | -1 // reset position to margins.left
- add title
{String}- const table = { title: "", };
- const options = { title: "", };
- add subtitle
{String}- const table = { subtitle: "", };
- const options = { subtitle: "", };
- add columnsSize on options = {} // only to simple table
- Function tableToJson
- import {tableToJson} from 'pdfkit-table';
- const table = tableToJson('#id_table');
{Object}
- Function allTablesToJson
- import {allTablesToJson} from 'pdfkit-table';
- const tables = allTablesToJson();
{Array}
- spacing cell and header alignment
- Thank you, contributors!
- renderer function on json file. { "renderer": "function(value, icol, irow, row){ return (value+1) +
(${(irow+2)}); }" } - fix width table and separation lines size
The MIT License.
|
Natan Cabral natancabral@hotmail.com https://github.com/natancabral/ |
- pdfkit - pdfkit
- ideas - giuseppe-santoro





