natancabral-pdfkit-table
v0.2.11
Published
PdfKit Table. Helps to draw informations in simple tables using pdfkit. #server-side. Generate pdf tables with TypeScript / JavaScript (PDFKIT plugin)
Maintainers
Readme
natancabral-pdfkit-table is now:
pdfkit-table
natancabral-pdfkit-tablehas been deprecated and will not receive any further updates, bug fixes, or security patches.Please migrate to
pdfkit-table
pdfkit-tableis the actively maintained successor — written in TypeScript, with a richer API, better performance, and ongoing support.Repository: https://github.com/natancabral/pdfkit-table
npm: https://www.npmjs.com/package/pdfkit-table
Migration to:
pdfkit-table
Generate PDF tables with TypeScript / JavaScript (PDFKit plugin)
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
Install
yarn add pdfkit-tablenpm install pdfkit-tableImport
All 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');
Using your own PDFKit
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.).
CommonJS
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();
})();TypeScript / ESM
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();
})();Use
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
})();Recipe examples
The Examples section at the top lists every script and PDF under example/. Below are the same patterns in short form for documentation.
Server example
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.
Example 1 — simple array (rows)
;(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();
})();Example 2 — data + rows, custom renderers
;(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();
})();Example 3 — JSON string (JSON.stringify → doc.table)
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();
})();Example 4 — JSON file (object or array)
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();
})();Table
- 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 Function function( value, indexColumn, indexRow, row, rectRow, rectCell ) { return value }
- data Array.<object>
- ~~datas~~ Array.<object> (deprecated — use
data) - rows Array.[]
- title String | Object
- subtitle String | Object
- options Object
- headers Array.<object> | Array.[]
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 ~~backgroundColor~~ | String | undefined | color of column |
| columnOpacity or ~~backgroundOpacity~~| Number | undefined | opacity of column |
| padding | Number | Array | Object | 0 | cell padding — overrides global padding. CSS shorthand: [top, right, bottom, left] |
| renderer | Function | Function | function( value, indexColumn, indexRow, row, rectRow, rectCell ) { return value } |
Simple headers example
const table = {
// simple headers only with ROWS (not DATA)
headers: ['Name', 'Age'],
// simple content
rows: [
['Jack', '32'], // row 1
['Maria', '30'], // row 2
]
};Complex headers example
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
]
};
Options
| Property | Type | Default | Description |
|---|---|---|---|
| title | String | Object | undefined | table title |
| subtitle | String | Object | undefined | table subtitle |
| width | Number | undefined | total table width |
| x | Number | null | undefined | x position. Pass null or -1 to reset to left margin |
| 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 | 0 | cell padding — CSS shorthand [top, right, bottom, left] |
| 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 |
Options example
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),
}Page-break control
// 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) |
doc.checkPageBreak(minHeight?) — prevent orphaned titles
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);Options Row
- separation {Boolean}
- color {String}
- columnColor {String}
- columnOpacity {Number}
- backgroundColor {String} (deprecated — use
columnColor) - backgroundOpacity {Number} (deprecated — use
columnOpacity) - 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' },
]Options Cell
- fontSize {Number}
- fontFamily {String}
- color {String}
data: [
// options cell — value is { label, options }
{
name: { label: 'Jack', options: { fontSize: 10, fontFamily: 'Courier-Bold' } },
},
]Fonts Family
- Courier
- Courier-Bold
- Courier-Oblique
- Courier-BoldOblique
- Helvetica
- Helvetica-Bold
- Helvetica-Oblique
- Helvetica-BoldOblique
- Symbol
- Times-Roman
- Times-Bold
- Times-Italic
- Times-BoldItalic
- ZapfDingbats
ToDo
- 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
Changelogs
0.2.11
- accept relative column sizes: (null, undefined or '*') JS | PDF
- columnsSize: [50, 300, null],
- columnsSize: [50, 300, undefined, 200],
- columnsSize: [100, '*', 50, null],
0.2.9
- 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');0.2.8
- RTL
- Thanks moshfeu
options: {
rtl: true, // boolean
}0.2.2
- 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 0.2.0
New features
- 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.
Bug fixes
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.
0.1.90
- Add options minRowHeight
- Thanks LouiseEH @LouiseEH
options: {
minRowHeight: 30, // pixel
}0.1.89
- Fix first line height
- Thanks José Luis Francisco @JoseLuis21
0.1.88
- 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 },
}0.1.87
- Add options hideHeader
- Thanks Ville @VilleKoo
options: {
hideHeader: true,
}0.1.86
- TypeScript (ts) interface (index.ts)
- Thanks Côte Arthur @CoteArthur
0.1.83
- Avoid a table title appearing alone
- Thanks Alexis Arriola @AlexisArriola
- Problem with long text in cell spreading on several pages
- Thanks Ed @MeMineToMe
0.1.72
- 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
0.1.70
- Fix y position.
- Thanks Nabil Tahmidul Karim @nabiltkarim
0.1.68
- 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)~~;0.1.63
- 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
0.1.60
- Add callback on addBackground function, add .save() and .restore() style.
- Header font color
- Thanks @dev-fema
0.1.59
- Add padding
0.1.57
- Header color and opacity
headers: [ {label:"Name", property:"name", headerColor:"#FF0000", headerOpacity:0.5 } ]- Thanks Albert Taveras @itsalb3rt
0.1.55
- Align on headers
headers: [ {label:"Name", property:"name", align:"center"} ]- Thanks Andrea Fucci
0.1.49
- Max size page
0.1.48
- Header height size
- Separate line width
0.1.47
- addHeader() function on all add pages
- Thanks Anders Wasen @QAnders
0.1.46
- addBackground() function to node 8
- Thanks @mehmetunubol
0.1.45
- Add rectCell on renderer
- renderer = ( value, indexColumn, indexRow, row, rectRow, rectCell ) => {}
- Thanks Eduardo Miranda
0.1.44
- Fix paddings and distances
0.1.43
- Remove rowSpacing
- Fix columnSpacing
0.1.41
- 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 } } }}, ]
0.1.39
- 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) } }
0.1.38
- tables {Function} - Add many tables.
- doc.tables([ table0, table1, table2, ... ]);
0.1.37
- addPage {Boolean} - Add table on new page.
- const options = { addPage: true, };
0.1.36
- Fix position x, y of title
- options.x: null | -1 // reset position to margins.left
0.1.35
- add title {String}
- const table = { title: "", };
- const options = { title: "", };
- add subtitle {String}
- const table = { subtitle: "", };
- const options = { subtitle: "", };
0.1.34
- add columnsSize on options = {} // only to simple table
0.1.33
- Function tableToJson
- import {tableToJson} from 'pdfkit-table';
- const table = tableToJson('#id_table'); {Object}
- Function allTablesToJson
- import {allTablesToJson} from 'pdfkit-table';
- const tables = allTablesToJson(); {Array}
0.1.32
- spacing cell and header alignment
- Thank you, contributors!
0.1.31
- renderer function on json file. { "renderer": "function(value, icol, irow, row){ return (value+1) +
(${(irow+2)}); }" } - fix width table and separation lines size
License
The MIT License.
Author
Thank you
- pdfkit - pdfkit
- ideas - giuseppe-santoro

