jspdf-utils 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/html-to-pdf.d.ts +84 -0
- package/dist/html-to-pdf.js +226 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# jsPDF Utils
|
|
2
|
+
|
|
3
|
+
HTML to PDF utilities for jsPDF with support for Arabic text, tables, and automatic page breaking.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install jspdf-utils jspdf html2canvas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import jsPDF from "jspdf";
|
|
15
|
+
import { renderHTML } from "jspdf-utils";
|
|
16
|
+
|
|
17
|
+
const doc = new jsPDF({ unit: "mm", format: "a4" });
|
|
18
|
+
|
|
19
|
+
// Add fonts if needed for Arabic/RTL text
|
|
20
|
+
doc.addFont("path/to/arial.ttf", "arial", "normal", "normal");
|
|
21
|
+
doc.addFont("path/to/arial-bold.ttf", "arial", "normal", "bold");
|
|
22
|
+
|
|
23
|
+
// Render HTML element to PDF
|
|
24
|
+
const element = document.getElementById("content");
|
|
25
|
+
await renderHTML(doc, element);
|
|
26
|
+
|
|
27
|
+
doc.save("output.pdf");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Automatic page breaking**: Prevents tables and text from being split awkwardly
|
|
33
|
+
- **Table splitting**: Large tables are split across pages with repeated headers
|
|
34
|
+
- **Text wrapping**: Long text blocks are intelligently broken at word boundaries
|
|
35
|
+
- **RTL/Arabic support**: Works with right-to-left languages when proper fonts are loaded
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
### Running the Example
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install
|
|
43
|
+
npm run dev
|
|
44
|
+
# Open http://localhost:5173
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
├── src/
|
|
51
|
+
│ └── html-to-pdf.js # Main utility functions
|
|
52
|
+
├── index.html # Example/demo page
|
|
53
|
+
└── package.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `renderHTML(doc, source, opts)`
|
|
59
|
+
|
|
60
|
+
Renders an HTML element to PDF.
|
|
61
|
+
|
|
62
|
+
- **doc**: jsPDF instance
|
|
63
|
+
- **source**: HTML element to render
|
|
64
|
+
- **opts**: Optional configuration (overrides defaults)
|
|
65
|
+
|
|
66
|
+
### `prepare(source, opts)`
|
|
67
|
+
|
|
68
|
+
Prepares an HTML element for rendering (used internally by renderHTML).
|
|
69
|
+
|
|
70
|
+
Returns: `{ clone, layout, options, cleanup }`
|
|
71
|
+
|
|
72
|
+
### Default Options
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
{
|
|
76
|
+
unit: 'mm',
|
|
77
|
+
format: 'a4',
|
|
78
|
+
pageWidth: 210,
|
|
79
|
+
pageHeight: 297,
|
|
80
|
+
margin: { top: 20, right: 20, bottom: 20, left: 20 }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsPDF doc.html() utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers that prepare an HTML element for clean, paginated PDF output
|
|
5
|
+
* via jsPDF's doc.html() renderer.
|
|
6
|
+
*/
|
|
7
|
+
import type { jsPDF } from "jspdf";
|
|
8
|
+
export interface Margin {
|
|
9
|
+
top: number;
|
|
10
|
+
right: number;
|
|
11
|
+
bottom: number;
|
|
12
|
+
left: number;
|
|
13
|
+
}
|
|
14
|
+
export type PageFormat = "a0" | "a1" | "a2" | "a3" | "a4" | "a5" | "a6" | "letter" | "legal" | "tabloid";
|
|
15
|
+
export interface PageOptions {
|
|
16
|
+
unit: string;
|
|
17
|
+
format: PageFormat;
|
|
18
|
+
pageWidth: number;
|
|
19
|
+
pageHeight: number;
|
|
20
|
+
margin: Margin;
|
|
21
|
+
}
|
|
22
|
+
/** Standard page dimensions in mm (portrait). */
|
|
23
|
+
declare const PAGE_SIZES: Record<PageFormat, [number, number]>;
|
|
24
|
+
/** Standard margins in mm per format. */
|
|
25
|
+
declare const PAGE_MARGINS: Record<PageFormat, number>;
|
|
26
|
+
export interface Layout {
|
|
27
|
+
renderedWidth: number;
|
|
28
|
+
scale: number;
|
|
29
|
+
contentWidthMm: number;
|
|
30
|
+
pageContentPx: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PrepareResult {
|
|
33
|
+
clone: HTMLElement;
|
|
34
|
+
layout: Layout;
|
|
35
|
+
options: PageOptions;
|
|
36
|
+
cleanup: () => void;
|
|
37
|
+
}
|
|
38
|
+
/** Compute derived layout values from options. */
|
|
39
|
+
declare function computeLayout(container: HTMLElement, opts: PageOptions): Layout;
|
|
40
|
+
/**
|
|
41
|
+
* Clone an element and position it off-screen at print width for measurement.
|
|
42
|
+
*/
|
|
43
|
+
declare function createPrintClone(source: HTMLElement, pageWidth?: number): HTMLElement;
|
|
44
|
+
/**
|
|
45
|
+
* Convert HTML table attributes (cellpadding, cellspacing, border) to
|
|
46
|
+
* inline CSS so doc.html()'s renderer picks them up.
|
|
47
|
+
*/
|
|
48
|
+
declare function normalizeTableAttributes(container: HTMLElement): void;
|
|
49
|
+
/**
|
|
50
|
+
* Split tables that exceed one page height into smaller sub-tables,
|
|
51
|
+
* repeating the header row in each chunk.
|
|
52
|
+
*
|
|
53
|
+
* Only operates on direct-child tables of `container`.
|
|
54
|
+
*/
|
|
55
|
+
declare function splitOversizedTables(container: HTMLElement, pageContentPx: number): void;
|
|
56
|
+
/**
|
|
57
|
+
* Split direct-child elements (non-table) that are taller than one page
|
|
58
|
+
* into word-boundary chunks using binary search.
|
|
59
|
+
*/
|
|
60
|
+
declare function splitOversizedText(container: HTMLElement, pageContentPx: number): void;
|
|
61
|
+
/** Insert spacer divs so that no direct child straddles a page boundary. */
|
|
62
|
+
declare function insertPageBreakSpacers(container: HTMLElement, pageContentPx: number): void;
|
|
63
|
+
/**
|
|
64
|
+
* Prepare an HTML element for doc.html() rendering.
|
|
65
|
+
*
|
|
66
|
+
* Clones the element, splits oversized tables/text, and inserts page-break
|
|
67
|
+
* spacers. Returns the ready-to-render clone and layout metadata.
|
|
68
|
+
*/
|
|
69
|
+
declare function prepare(source: HTMLElement, opts?: Partial<PageOptions>): PrepareResult;
|
|
70
|
+
/**
|
|
71
|
+
* Render an HTML element to PDF using doc.html().
|
|
72
|
+
*/
|
|
73
|
+
declare function renderHTML(doc: jsPDF, source: HTMLElement, opts?: Partial<PageOptions>): Promise<jsPDF>;
|
|
74
|
+
export interface ImagePDFOptions {
|
|
75
|
+
imageFormat?: "JPEG" | "PNG";
|
|
76
|
+
imageQuality?: number;
|
|
77
|
+
scale?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Render an HTML element as an image-based PDF. Each page is a rasterized
|
|
81
|
+
* screenshot — no selectable or extractable text in the output.
|
|
82
|
+
*/
|
|
83
|
+
declare function renderImagePDF(source: HTMLElement, opts?: Partial<PageOptions> & ImagePDFOptions): Promise<jsPDF>;
|
|
84
|
+
export { PAGE_SIZES, PAGE_MARGINS, computeLayout, createPrintClone, normalizeTableAttributes, splitOversizedTables, splitOversizedText, insertPageBreakSpacers, prepare, renderHTML, renderImagePDF, };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import N from "html2canvas";
|
|
2
|
+
const T = {
|
|
3
|
+
a0: [841, 1189],
|
|
4
|
+
a1: [594, 841],
|
|
5
|
+
a2: [420, 594],
|
|
6
|
+
a3: [297, 420],
|
|
7
|
+
a4: [210, 297],
|
|
8
|
+
a5: [148, 210],
|
|
9
|
+
a6: [105, 148],
|
|
10
|
+
letter: [215.9, 279.4],
|
|
11
|
+
legal: [215.9, 355.6],
|
|
12
|
+
tabloid: [279.4, 431.8]
|
|
13
|
+
}, B = {
|
|
14
|
+
a0: 40,
|
|
15
|
+
a1: 35,
|
|
16
|
+
a2: 30,
|
|
17
|
+
a3: 25,
|
|
18
|
+
a4: 25,
|
|
19
|
+
a5: 20,
|
|
20
|
+
a6: 12,
|
|
21
|
+
letter: 25.4,
|
|
22
|
+
legal: 25.4,
|
|
23
|
+
tabloid: 25
|
|
24
|
+
};
|
|
25
|
+
function x(i = {}) {
|
|
26
|
+
const o = i.format ?? "a4", [n, t] = T[o], r = i.pageWidth ?? n, e = i.pageHeight ?? t, a = B[o], c = {
|
|
27
|
+
top: a,
|
|
28
|
+
right: a,
|
|
29
|
+
bottom: a,
|
|
30
|
+
left: a
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
unit: i.unit ?? "mm",
|
|
34
|
+
format: o,
|
|
35
|
+
pageWidth: r,
|
|
36
|
+
pageHeight: e,
|
|
37
|
+
margin: { ...c, ...i.margin }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function A(i, o) {
|
|
41
|
+
const n = i.offsetWidth, t = o.pageWidth - o.margin.left - o.margin.right, r = t / n, a = (o.pageHeight - o.margin.top - o.margin.bottom) / r;
|
|
42
|
+
return { renderedWidth: n, scale: r, contentWidthMm: t, pageContentPx: a };
|
|
43
|
+
}
|
|
44
|
+
function v(i, o = 210) {
|
|
45
|
+
const n = i.cloneNode(!0);
|
|
46
|
+
return Object.assign(n.style, {
|
|
47
|
+
position: "fixed",
|
|
48
|
+
top: "0",
|
|
49
|
+
left: "0",
|
|
50
|
+
boxSizing: "border-box",
|
|
51
|
+
width: o + "mm",
|
|
52
|
+
opacity: "0.000001",
|
|
53
|
+
pointerEvents: "none"
|
|
54
|
+
}), document.body.appendChild(n), n;
|
|
55
|
+
}
|
|
56
|
+
function W(i) {
|
|
57
|
+
for (const o of i.querySelectorAll("table")) {
|
|
58
|
+
const n = o.getAttribute("cellpadding");
|
|
59
|
+
if (n) {
|
|
60
|
+
for (const t of o.querySelectorAll("th, td"))
|
|
61
|
+
t.style.padding || (t.style.padding = n + "px");
|
|
62
|
+
o.removeAttribute("cellpadding");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function C(i, o) {
|
|
67
|
+
for (const n of Array.from(
|
|
68
|
+
i.querySelectorAll(":scope > table")
|
|
69
|
+
)) {
|
|
70
|
+
if (n.offsetHeight <= o) continue;
|
|
71
|
+
const t = Array.from(n.rows);
|
|
72
|
+
if (t.length === 0) continue;
|
|
73
|
+
const r = t[0].querySelector("th") !== null, e = r ? t[0] : null, a = r ? t.slice(1) : t, c = e ? e.offsetHeight : 0, f = o - c - 2, h = [];
|
|
74
|
+
let l = [], d = 0;
|
|
75
|
+
for (const s of a) {
|
|
76
|
+
const g = s.offsetHeight;
|
|
77
|
+
d + g > f && l.length > 0 && (h.push(l), l = [], d = 0), l.push(s), d += g;
|
|
78
|
+
}
|
|
79
|
+
l.length > 0 && h.push(l);
|
|
80
|
+
for (const s of h) {
|
|
81
|
+
const g = n.cloneNode(!1);
|
|
82
|
+
e && g.appendChild(e.cloneNode(!0));
|
|
83
|
+
for (const m of s) g.appendChild(m.cloneNode(!0));
|
|
84
|
+
n.parentNode.insertBefore(g, n);
|
|
85
|
+
}
|
|
86
|
+
n.remove();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function P(i, o) {
|
|
90
|
+
for (const n of Array.from(i.querySelectorAll(":scope > *"))) {
|
|
91
|
+
const t = n;
|
|
92
|
+
if (t.offsetHeight <= o || t.tagName === "TABLE")
|
|
93
|
+
continue;
|
|
94
|
+
const r = t.tagName, e = t.getAttribute("style") || "", a = getComputedStyle(t).width, c = (t.textContent || "").split(/\s+/).filter(Boolean), f = document.createElement(r);
|
|
95
|
+
f.setAttribute("style", e), Object.assign(f.style, {
|
|
96
|
+
position: "absolute",
|
|
97
|
+
visibility: "hidden",
|
|
98
|
+
width: a
|
|
99
|
+
}), i.appendChild(f);
|
|
100
|
+
const h = [];
|
|
101
|
+
let l = 0;
|
|
102
|
+
for (; l < c.length; ) {
|
|
103
|
+
let d = l + 1, s = c.length;
|
|
104
|
+
for (; d < s; ) {
|
|
105
|
+
const m = Math.ceil((d + s) / 2);
|
|
106
|
+
f.textContent = c.slice(l, m).join(" "), f.offsetHeight <= o ? d = m : s = m - 1;
|
|
107
|
+
}
|
|
108
|
+
const g = document.createElement(r);
|
|
109
|
+
g.setAttribute("style", e), g.textContent = c.slice(l, d).join(" "), h.push(g), l = d;
|
|
110
|
+
}
|
|
111
|
+
f.remove();
|
|
112
|
+
for (const d of h)
|
|
113
|
+
t.parentNode.insertBefore(d, t);
|
|
114
|
+
t.remove();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function M(i, o) {
|
|
118
|
+
const n = Array.from(i.children);
|
|
119
|
+
for (const t of n) {
|
|
120
|
+
const r = t.offsetTop, e = r + t.offsetHeight, a = (Math.floor(r / o) + 1) * o;
|
|
121
|
+
if (e > a && t.offsetHeight <= o) {
|
|
122
|
+
const c = document.createElement("div");
|
|
123
|
+
c.style.height = a - r + 1 + "px", t.parentNode.insertBefore(c, t);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function j(i, o = {}) {
|
|
128
|
+
const n = x(o), t = v(i, n.pageWidth);
|
|
129
|
+
W(t);
|
|
130
|
+
const r = A(t, n);
|
|
131
|
+
return C(t, r.pageContentPx), P(t, r.pageContentPx), M(t, r.pageContentPx), {
|
|
132
|
+
clone: t,
|
|
133
|
+
layout: r,
|
|
134
|
+
options: n,
|
|
135
|
+
cleanup: () => t.remove()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function D(i, o, n = {}) {
|
|
139
|
+
const { clone: t, layout: r, options: e, cleanup: a } = j(o, n);
|
|
140
|
+
try {
|
|
141
|
+
await new Promise((c) => {
|
|
142
|
+
i.html(t, {
|
|
143
|
+
callback: () => c(),
|
|
144
|
+
width: r.contentWidthMm,
|
|
145
|
+
windowWidth: r.renderedWidth,
|
|
146
|
+
margin: [
|
|
147
|
+
e.margin.top,
|
|
148
|
+
e.margin.right,
|
|
149
|
+
e.margin.bottom,
|
|
150
|
+
e.margin.left
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
} finally {
|
|
155
|
+
a();
|
|
156
|
+
}
|
|
157
|
+
return i;
|
|
158
|
+
}
|
|
159
|
+
async function F(i, o = {}) {
|
|
160
|
+
const { imageFormat: n = "JPEG", imageQuality: t = 1, scale: r = 2 } = o, e = x(o), a = v(i, e.pageWidth);
|
|
161
|
+
a.style.opacity = "1", a.style.left = "-99999px", W(a);
|
|
162
|
+
const c = A(a, e);
|
|
163
|
+
C(a, c.pageContentPx), P(a, c.pageContentPx), M(a, c.pageContentPx);
|
|
164
|
+
try {
|
|
165
|
+
const f = await N(a, {
|
|
166
|
+
scale: r,
|
|
167
|
+
backgroundColor: "#ffffff"
|
|
168
|
+
}), { jsPDF: h } = await import("jspdf"), l = e.pageWidth - e.margin.left - e.margin.right, d = e.pageHeight - e.margin.top - e.margin.bottom, s = f.width, g = d / l * s, m = Math.ceil(f.height / g), H = e.pageWidth > e.pageHeight ? "l" : "p", w = new h({
|
|
169
|
+
orientation: H,
|
|
170
|
+
unit: "mm",
|
|
171
|
+
format: [e.pageWidth, e.pageHeight]
|
|
172
|
+
});
|
|
173
|
+
for (let u = 0; u < m; u++) {
|
|
174
|
+
const p = Math.min(
|
|
175
|
+
g,
|
|
176
|
+
f.height - u * g
|
|
177
|
+
), y = document.createElement("canvas");
|
|
178
|
+
y.width = s, y.height = p;
|
|
179
|
+
const b = y.getContext("2d");
|
|
180
|
+
if (!b) throw new Error("Could not get canvas context");
|
|
181
|
+
b.fillStyle = "#ffffff", b.fillRect(0, 0, s, p), b.drawImage(
|
|
182
|
+
f,
|
|
183
|
+
0,
|
|
184
|
+
u * g,
|
|
185
|
+
s,
|
|
186
|
+
p,
|
|
187
|
+
0,
|
|
188
|
+
0,
|
|
189
|
+
s,
|
|
190
|
+
p
|
|
191
|
+
);
|
|
192
|
+
const E = y.toDataURL(
|
|
193
|
+
`image/${n.toLowerCase()}`,
|
|
194
|
+
t
|
|
195
|
+
);
|
|
196
|
+
u > 0 && w.addPage([e.pageWidth, e.pageHeight], H);
|
|
197
|
+
const S = p / s * l;
|
|
198
|
+
w.addImage(
|
|
199
|
+
E,
|
|
200
|
+
n,
|
|
201
|
+
e.margin.left,
|
|
202
|
+
e.margin.top,
|
|
203
|
+
l,
|
|
204
|
+
S,
|
|
205
|
+
void 0,
|
|
206
|
+
"FAST"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return w;
|
|
210
|
+
} finally {
|
|
211
|
+
a.remove();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
B as PAGE_MARGINS,
|
|
216
|
+
T as PAGE_SIZES,
|
|
217
|
+
A as computeLayout,
|
|
218
|
+
v as createPrintClone,
|
|
219
|
+
M as insertPageBreakSpacers,
|
|
220
|
+
W as normalizeTableAttributes,
|
|
221
|
+
j as prepare,
|
|
222
|
+
D as renderHTML,
|
|
223
|
+
F as renderImagePDF,
|
|
224
|
+
C as splitOversizedTables,
|
|
225
|
+
P as splitOversizedText
|
|
226
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jspdf-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utility helpers for jsPDF's doc.html() renderer with automatic page breaking, table splitting, and RTL support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/html-to-pdf.js",
|
|
7
|
+
"types": "dist/html-to-pdf.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/html-to-pdf.d.ts",
|
|
14
|
+
"import": "./dist/html-to-pdf.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"jspdf",
|
|
19
|
+
"html-to-pdf",
|
|
20
|
+
"pdf",
|
|
21
|
+
"page-break",
|
|
22
|
+
"table-split",
|
|
23
|
+
"rtl",
|
|
24
|
+
"arabic"
|
|
25
|
+
],
|
|
26
|
+
"author": "Muaath <muath.ghassan98@gmail.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/maath9826/jsPDF-utils.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/maath9826/jsPDF-utils/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/maath9826/jsPDF-utils#readme",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"html2canvas": "^1.4.1",
|
|
38
|
+
"jspdf": "^4.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "vite",
|
|
42
|
+
"build": "vite build && tsc --emitDeclarationOnly",
|
|
43
|
+
"preview": "vite preview",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"html2canvas": "^1.4.1",
|
|
49
|
+
"jspdf": "^4.1.0",
|
|
50
|
+
"typescript": "^5.7.0",
|
|
51
|
+
"vite": "^6.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|