Skip to content

Commit cb353d4

Browse files
fix: make pdf export dependency free
1 parent 7c28d02 commit cb353d4

9 files changed

Lines changed: 183 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 2.0.3 - 2026-07-05
4+
5+
### Fixed
6+
7+
- Replaced the default PDF export implementation with a dependency-free browser PDF writer so Angular apps do not need jsPDF optional HTML/canvas dependencies for basic table export.
8+
- Removed `jspdf` and `jspdf-autotable` from optional peer dependencies.
9+
310
## 2.0.2 - 2026-07-05
411

512
### Fixed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,21 @@ npm install @angular-bootstrap/ngbootstrap bootstrap bootstrap-icons
2929
Optional integrations are installed only when you use those features:
3030

3131
```bash
32-
npm install chart.js jspdf jspdf-autotable
32+
npm install chart.js
3333
```
3434

35+
PDF and Excel export are dependency-free by default.
36+
3537
Excel export is dependency-free by default. The built-in `BrowserExcelExportAdapter`
3638
generates an Excel-compatible workbook in the browser and avoids unmaintained
3739
spreadsheet writer dependencies. It is intended for visible column values and
3840
basic scalar cell types; use a custom `ExcelExportAdapter` for formulas, charts,
3941
multiple sheets, workbook styling, or other advanced workbook features.
4042

43+
The built-in PDF adapter generates a simple table PDF in the browser. Provide a custom
44+
`PdfExportAdapter` when your product needs branded PDFs, images, charts, advanced layout,
45+
rich typography, headers, footers, or more precise pagination.
46+
4147
## Use
4248

4349
Import standalone components directly in your Angular component.

RELEASE_NOTES.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
# @angular-bootstrap/ngbootstrap 2.0.2
1+
# @angular-bootstrap/ngbootstrap 2.0.3
22

3-
This patch fixes PDF export bundling for Angular browser apps.
3+
This patch makes the default PDF export path dependency-free.
44

55
## Fixed
66

7-
- `JsPdfAdapter` now loads jsPDF from its browser UMD bundle.
8-
- This avoids forcing consuming apps to resolve jsPDF optional ESM dependencies such as HTML/canvas sanitization helpers when they only need table PDF export.
7+
- `JsPdfAdapter` now generates a simple table PDF directly in the browser.
8+
- Removed `jspdf` and `jspdf-autotable` from optional peer dependencies.
9+
- Angular apps no longer need jsPDF optional HTML/canvas dependencies for basic DataGrid PDF export.
910

1011
## Notes
1112

12-
Applications using PDF export should keep both maintained PDF integrations installed:
13+
PDF and Excel export are dependency-free by default. The built-in PDF adapter is intended for simple table export only.
1314

14-
```bash
15-
npm install jspdf jspdf-autotable
16-
```
17-
18-
Excel export remains dependency-free through `BrowserExcelExportAdapter`.
15+
Use a custom `PdfExportAdapter` for branded layouts, images, charts, rich typography, or advanced pagination.

package.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular-bootstrap/ngbootstrap",
3-
"version": "2.0.2",
3+
"version": "2.0.3",
44
"description": "Bootstrap-friendly standalone Angular UI components, including DataGrid, pagination, typeahead, tree, splitter, stepper, chips, and drag-and-drop.",
55
"author": {
66
"name": "Harmeet Singh"
@@ -62,19 +62,11 @@
6262
"@angular/core": ">=21.0.0 <23.0.0",
6363
"@angular/forms": ">=21.0.0 <23.0.0",
6464
"chart.js": ">=4.4.0 <5.0.0",
65-
"jspdf": ">=4.2.1 <5.0.0",
66-
"jspdf-autotable": ">=5.0.0 <6.0.0",
6765
"rxjs": "^7.8.0"
6866
},
6967
"peerDependenciesMeta": {
7068
"chart.js": {
7169
"optional": true
72-
},
73-
"jspdf": {
74-
"optional": true
75-
},
76-
"jspdf-autotable": {
77-
"optional": true
7870
}
7971
},
8072
"dependencies": {

scripts/validate-release-package.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const distDir = join(root, 'dist', 'ngbootstrap');
66
const sourcePkgPath = join(root, 'package.json');
77
const distPkgPath = join(distDir, 'package.json');
88
const expectedPackageName = '@angular-bootstrap/ngbootstrap';
9-
const requiredOptionalPeers = ['chart.js', 'jspdf', 'jspdf-autotable'];
9+
const requiredOptionalPeers = ['chart.js'];
1010
const requiredAngularPeers = ['@angular/common', '@angular/core', '@angular/forms'];
11-
const forbiddenPeerNames = ['x' + 'lsx'];
11+
const forbiddenPeerNames = ['x' + 'lsx', 'jspdf', 'jspdf-autotable'];
1212

1313
function readJson(path) {
1414
return JSON.parse(readFileSync(path, 'utf8'));
@@ -46,8 +46,6 @@ for (const peerName of requiredAngularPeers) {
4646
assert(sourcePkg.peerDependencies?.[peerName] === '>=21.0.0 <23.0.0', `${peerName} peer range must remain >=21.0.0 <23.0.0.`);
4747
}
4848

49-
assert(sourcePkg.peerDependencies?.jspdf === '>=4.2.1 <5.0.0', 'jspdf peer range must require the patched 4.2.1+ release.');
50-
5149
for (const peerName of requiredOptionalPeers) {
5250
assert(sourcePkg.peerDependencies?.[peerName], `${peerName} must remain declared as a peer dependency.`);
5351
assert(sourcePkg.peerDependenciesMeta?.[peerName]?.optional === true, `${peerName} must remain an optional peer dependency.`);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { JsPdfAdapter } from './jsdf.adapter';
2+
3+
describe('JsPdfAdapter', () => {
4+
it('creates a dependency-free PDF document', () => {
5+
const adapter = new JsPdfAdapter() as unknown as {
6+
createPdf(payload: {
7+
fileName: string;
8+
columns: string[];
9+
rows: Array<Record<string, unknown>>;
10+
options?: { landscape?: boolean };
11+
}): string;
12+
};
13+
14+
const pdf = adapter.createPdf({
15+
fileName: 'users',
16+
columns: ['name', 'role'],
17+
rows: [
18+
{ name: 'Ava Patel', role: 'Admin' },
19+
{ name: 'Noah Chen', role: 'Editor' },
20+
],
21+
options: { landscape: true },
22+
});
23+
24+
expect(pdf.startsWith('%PDF-1.4')).toBe(true);
25+
expect(pdf).toContain('/Type /Catalog');
26+
expect(pdf).toContain('(users) Tj');
27+
expect(pdf).toContain('(Ava Patel');
28+
expect(pdf).toContain('xref');
29+
expect(pdf).toContain('%%EOF');
30+
});
31+
});
Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,133 @@
11
import { PdfExportAdapter, PdfExportPayload } from '../services/export.services';
22

3+
const PAGE_SIZES: Record<string, [number, number]> = {
4+
A4: [595.28, 841.89],
5+
Letter: [612, 792],
6+
};
7+
38
export class JsPdfAdapter implements PdfExportAdapter {
4-
async export({ fileName, columns, rows, options }: PdfExportPayload) {
5-
const { jsPDF } = await import('jspdf/dist/jspdf.umd.min.js');
6-
const autoTable = (await import('jspdf-autotable')).default;
7-
const doc = new jsPDF({ orientation: options?.landscape ? 'landscape' : 'portrait' });
8-
autoTable(doc, { head: [columns], body: rows.map(r => columns.map(k => r[k])), margin: options?.margins });
9-
doc.save(`${fileName}.pdf`);
9+
async export(payload: PdfExportPayload) {
10+
const pdf = this.createPdf(payload);
11+
const blob = new Blob([pdf], { type: 'application/pdf' });
12+
const url = URL.createObjectURL(blob);
13+
const link = document.createElement('a');
14+
link.href = url;
15+
link.download = `${payload.fileName}.pdf`;
16+
link.click();
17+
URL.revokeObjectURL(url);
18+
}
19+
20+
private createPdf({ fileName, columns, rows, options }: PdfExportPayload): string {
21+
const requestedSize = String(options?.pageSize || 'A4');
22+
const baseSize = PAGE_SIZES[requestedSize] || PAGE_SIZES.A4;
23+
const landscape = Boolean(options?.landscape);
24+
const [pageWidth, pageHeight] = landscape ? [baseSize[1], baseSize[0]] : baseSize;
25+
const margin = this.resolveMargin(options?.margins);
26+
const lineHeight = 14;
27+
const maxLines = Math.max(1, Math.floor((pageHeight - margin.top - margin.bottom - 52) / lineHeight));
28+
const tableLines = this.toTableLines(columns, rows);
29+
const pages = this.chunk(tableLines, maxLines);
30+
const objects: string[] = [];
31+
const pageIds: number[] = [];
32+
33+
objects.push('<< /Type /Catalog /Pages 2 0 R >>');
34+
objects.push('');
35+
36+
for (const [index, lines] of pages.entries()) {
37+
const pageObjectId = objects.length + 1;
38+
const contentObjectId = pageObjectId + 1;
39+
pageIds.push(pageObjectId);
40+
objects.push(
41+
`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageWidth} ${pageHeight}] /Resources << /Font << /F1 ${pages.length * 2 + 3} 0 R >> >> /Contents ${contentObjectId} 0 R >>`
42+
);
43+
objects.push(this.contentStream(fileName, lines, index + 1, pages.length, pageHeight, margin, lineHeight));
44+
}
45+
46+
objects[1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`;
47+
objects.push('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>');
48+
49+
return this.serializePdf(objects);
50+
}
51+
52+
private resolveMargin(margins?: [number, number, number, number]) {
53+
if (!Array.isArray(margins) || margins.length !== 4) {
54+
return { top: 40, right: 40, bottom: 40, left: 40 };
55+
}
56+
const [top, right, bottom, left] = margins.map(value => Math.max(16, Number(value) || 40));
57+
return { top, right, bottom, left };
58+
}
59+
60+
private toTableLines(columns: string[], rows: any[]): string[] {
61+
const headers = columns.map(column => this.printable(column));
62+
const body = rows.map(row => columns.map(column => this.printable(row?.[column])));
63+
const widths = headers.map((header, index) =>
64+
Math.min(
65+
24,
66+
Math.max(
67+
header.length,
68+
...body.map(values => values[index]?.length || 0)
69+
)
70+
)
71+
);
72+
const format = (values: string[]) => values.map((value, index) => value.padEnd(widths[index]).slice(0, widths[index])).join(' ');
73+
return [
74+
format(headers),
75+
widths.map(width => '-'.repeat(width)).join(' '),
76+
...body.map(format),
77+
];
78+
}
79+
80+
private contentStream(
81+
fileName: string,
82+
lines: string[],
83+
page: number,
84+
totalPages: number,
85+
pageHeight: number,
86+
margin: { top: number; left: number },
87+
lineHeight: number
88+
): string {
89+
const commands = [
90+
'BT',
91+
`/F1 16 Tf ${margin.left} ${pageHeight - margin.top} Td (${this.pdfText(fileName)}) Tj`,
92+
`/F1 9 Tf 0 -24 Td (${this.pdfText(`Page ${page} of ${totalPages}`)}) Tj`,
93+
...lines.map(line => `0 -${lineHeight} Td (${this.pdfText(line)}) Tj`),
94+
'ET',
95+
].join('\n');
96+
return `<< /Length ${commands.length} >>\nstream\n${commands}\nendstream`;
97+
}
98+
99+
private chunk<T>(items: T[], size: number): T[][] {
100+
const pages: T[][] = [];
101+
for (let index = 0; index < items.length; index += size) {
102+
pages.push(items.slice(index, index + size));
103+
}
104+
return pages.length ? pages : [[]];
105+
}
106+
107+
private printable(value: unknown): string {
108+
if (value === null || value === undefined) {
109+
return '';
110+
}
111+
return String(value).replace(/[^\x20-\x7E]/g, '?').replace(/\s+/g, ' ').trim();
112+
}
113+
114+
private pdfText(value: string): string {
115+
return value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
116+
}
117+
118+
private serializePdf(objects: string[]): string {
119+
let pdf = '%PDF-1.4\n';
120+
const offsets = [0];
121+
for (const [index, object] of objects.entries()) {
122+
offsets[index + 1] = pdf.length;
123+
pdf += `${index + 1} 0 obj\n${object}\nendobj\n`;
124+
}
125+
const xrefOffset = pdf.length;
126+
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
127+
for (let index = 1; index <= objects.length; index++) {
128+
pdf += `${String(offsets[index]).padStart(10, '0')} 00000 n \n`;
129+
}
130+
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`;
131+
return pdf;
10132
}
11133
}

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference path="./types/optional-peer-deps.d.ts" />
21
export * from './datagrid';
32
export * from './drag-drop';
43
export * from './pagination';

src/types/optional-peer-deps.d.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)