smart-pdf-js 1.0.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 +208 -0
- package/index.d.ts +6 -0
- package/package.json +28 -0
- package/src/generatePDF.js +202 -0
- package/src/index.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Smart PDF
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/smart-pdf)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.npmjs.com/package/smart-pdf)
|
|
6
|
+
|
|
7
|
+
**The ultimate DOM-to-PDF solution for modern web applications.**
|
|
8
|
+
|
|
9
|
+
`smart-pdf` is a powerful, lightweight, and developer-friendly wrapper around `html2canvas` and `jspdf`. It simplifies the complex process of generating multi-page PDFs from HTML, making it easy to turn your React, Next.js, or vanilla JavaScript web pages into professional-quality PDF documents.
|
|
10
|
+
|
|
11
|
+
Unlike standard libraries, `smart-pdf` comes with built-in support for modern CSS features like **OKLCH colors**, ensuring your PDF looks exactly as intended.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Key Features
|
|
16
|
+
|
|
17
|
+
- **Multi-page Generation**: Effortlessly split your content across multiple pages using simple CSS selectors.
|
|
18
|
+
- ** OKLCH Color Support**: First-class support for modern CSS color spaces, automatically converting OKLCH to RGB for perfect rendering.
|
|
19
|
+
- ** React & Next.js Ready**: Designed to work seamlessly with modern component-based frameworks.
|
|
20
|
+
- ** Highly Configurable**: Full control over margins, orientation, formats (A4, Letter, etc.), and image quality.
|
|
21
|
+
- ** Lightweight**: Zero configuration needed for most use cases—just install and generate.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
Install the package via npm, yarn, or pnpm:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# npm
|
|
31
|
+
npm install smart-pdf
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 💡 Usage
|
|
38
|
+
|
|
39
|
+
### 1. Vanilla JavaScript / Basic Usage
|
|
40
|
+
|
|
41
|
+
The simplest way to use `smart-pdf` is to define the sections you want to capture and call `generatePDF`.
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { generatePDF } from "smart-pdf";
|
|
45
|
+
|
|
46
|
+
const config = {
|
|
47
|
+
filename: "monthly-report.pdf",
|
|
48
|
+
pages: [
|
|
49
|
+
{
|
|
50
|
+
// First page content from element with ID 'page1'
|
|
51
|
+
from: "#page1-start",
|
|
52
|
+
to: "#page1-end",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
// Second page content
|
|
56
|
+
from: ".section-2-start",
|
|
57
|
+
to: ".section-2-end",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
document.getElementById("download-btn").addEventListener("click", () => {
|
|
63
|
+
generatePDF(config).then(() => {
|
|
64
|
+
console.log("PDF successfully downloaded!");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### 2. Using with React
|
|
72
|
+
|
|
73
|
+
In React, you can use `useRef` to target elements, but using unique IDs or classes is often simpler for dynamic content.
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
import React from "react";
|
|
77
|
+
import { generatePDF } from "smart-pdf";
|
|
78
|
+
|
|
79
|
+
const PDFReport = () => {
|
|
80
|
+
const handleDownload = async () => {
|
|
81
|
+
const config = {
|
|
82
|
+
filename: "react-report.pdf",
|
|
83
|
+
pages: [{ from: "#report-header", to: "#report-footer" }],
|
|
84
|
+
jsPDFOptions: { orientation: "landscape" },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await generatePDF(config);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<div id="report-header">
|
|
93
|
+
<h1>Monthly Analytics</h1>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Your complex content charts, tables, etc. */}
|
|
97
|
+
<div className="content">
|
|
98
|
+
<p>This content will be included in the PDF.</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div id="report-footer">
|
|
102
|
+
<p>End of Report</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<button onClick={handleDownload} className="btn-primary">
|
|
106
|
+
Download PDF
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default PDFReport;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### 3. Using with Next.js (App Router & Pages Router)
|
|
118
|
+
|
|
119
|
+
Since `smart-pdf` relies on browser APIs (`window`, `document`), it must be dynamically imported with `ssr: false` or used inside `useEffect` / event handlers to avoid Server-Side Rendering (SSR) errors like `ReferenceError: document is not defined`.
|
|
120
|
+
|
|
121
|
+
#### Method A: Inside a Client Component (Recommended for App Router)
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
"use client"; // Important for App Router
|
|
125
|
+
|
|
126
|
+
import React from "react";
|
|
127
|
+
|
|
128
|
+
// You can import directly if function is called on an event (click)
|
|
129
|
+
// as the import will be resolved on the client.
|
|
130
|
+
|
|
131
|
+
import { generatePDF } from "smart-pdf";
|
|
132
|
+
|
|
133
|
+
export default function InvoicePage() {
|
|
134
|
+
const downloadInvoice = async () => {
|
|
135
|
+
// Alternatively, dynamic import inside the function for cleaner bundle splitting
|
|
136
|
+
// const { generatePDF } = await import('smart-pdf');
|
|
137
|
+
|
|
138
|
+
await generatePDF({
|
|
139
|
+
filename: "invoice-123.pdf",
|
|
140
|
+
pages: [{ from: ".invoice-start", to: ".invoice-end" }],
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<main>
|
|
146
|
+
<div className="invoice-start">Invoice Header</div>
|
|
147
|
+
{/* Invoice Details */}
|
|
148
|
+
<div className="invoice-end">Thank you for your business</div>
|
|
149
|
+
|
|
150
|
+
<button onClick={downloadInvoice}>Download Invoice</button>
|
|
151
|
+
</main>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Method B: Dynamic Import Component
|
|
157
|
+
|
|
158
|
+
If you need to render the component conditionally or simply want to ensure isolation:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
import dynamic from "next/dynamic";
|
|
162
|
+
|
|
163
|
+
const PDFButton = dynamic(() => import("../components/PDFButton"), {
|
|
164
|
+
ssr: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export default function Page() {
|
|
168
|
+
return <PDFButton />;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## ⚙️ Configuration (API Reference)
|
|
175
|
+
|
|
176
|
+
The `generatePDF(config)` function accepts a configuration object.
|
|
177
|
+
|
|
178
|
+
### `PDFConfig` Object
|
|
179
|
+
|
|
180
|
+
| Property | Type | Default | Description |
|
|
181
|
+
| :----------------------- | :------- | :----------------------------- | :-------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| **`pages`** | `Array` | **Required** | An array of objects defining the content for each page. |
|
|
183
|
+
| **`filename`** | `string` | `"document.pdf"` | The name of the output file. |
|
|
184
|
+
| **`jsPDFOptions`** | `object` | `{ unit: 'mm', format: 'a4' }` | Options passed to `new jsPDF()`. See [jsPDF docs](http://raw.githack.com/MrRio/jsPDF/master/docs/). |
|
|
185
|
+
| **`html2canvasOptions`** | `object` | `{ scale: 2, useCORS: true }` | Options passed to `html2canvas()`. `scale: 2` improves quality for retina displays. |
|
|
186
|
+
|
|
187
|
+
### `pages` Array Item
|
|
188
|
+
|
|
189
|
+
| Property | Type | Description |
|
|
190
|
+
| :--------- | :------- | :--------------------------------------------------------------------------------------- |
|
|
191
|
+
| **`from`** | `string` | **CSS Selector** (e.g., `#start-id`, `.class-name`) marking the **beginning** of a page. |
|
|
192
|
+
| **`to`** | `string` | **CSS Selector** marking the **end** of a page. |
|
|
193
|
+
|
|
194
|
+
> **Note:** The library captures everything _between_ the `from` and `to` elements (inclusive) in the DOM order.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Best Practices & Tips
|
|
199
|
+
|
|
200
|
+
1. **Image Loading (CORS)**: If your PDF contains images from external URLs, you might face CORS issues.
|
|
201
|
+
- Ensure your images are served with `Access-Control-Allow-Origin: *`.
|
|
202
|
+
- Set `html2canvasOptions: { useCORS: true }` in the config (enabled by default).
|
|
203
|
+
2. **Hiding Elements**: To hide buttons or controls in the generated PDF, use a CSS class (e.g., `.no-print`) and apply `opacity: 0` or unique styling during captured rendering, or temporary DOM manipulation.
|
|
204
|
+
3. **Fonts**: For custom fonts to render correctly, ensure they are fully loaded before generating the PDF. `document.fonts.ready` can be useful.
|
|
205
|
+
|
|
206
|
+
## 📄 License
|
|
207
|
+
|
|
208
|
+
MIT © [Rashedul Haque Rasel](https://github.com/RashedulHaqueRasel1)
|
package/index.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-pdf-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A powerful and easy-to-use library to generate PDFs from DOM elements, with support for OKLCH colors and multi-page documents.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pdf",
|
|
8
|
+
"html2canvas",
|
|
9
|
+
"jspdf",
|
|
10
|
+
"dom-to-pdf",
|
|
11
|
+
"oklch",
|
|
12
|
+
"pdf-generation"
|
|
13
|
+
],
|
|
14
|
+
"author": "Rashedul Haque Rasel",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/RashedulHaqueRasel1/smart-pdf.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/RashedulHaqueRasel1/smart-pdf/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/RashedulHaqueRasel1/smart-pdf#readme",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"html2canvas": "^1.4.1",
|
|
26
|
+
"jspdf": "^2.5.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import html2canvas from "html2canvas";
|
|
2
|
+
import jsPDF from "jspdf";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates the configuration object.
|
|
6
|
+
* @param {Object} config - The configuration object.
|
|
7
|
+
* @throws {Error} If the configuration is invalid.
|
|
8
|
+
*/
|
|
9
|
+
function validateConfig(config) {
|
|
10
|
+
if (!config || typeof config !== "object") {
|
|
11
|
+
throw new Error("Configuration object is required.");
|
|
12
|
+
}
|
|
13
|
+
if (!config.pages || !Array.isArray(config.pages) || config.pages.length === 0) {
|
|
14
|
+
throw new Error("Configuration must include a non-empty 'pages' array.");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts content from the DOM based on selectors.
|
|
20
|
+
* @param {string} from - Selector for the start element.
|
|
21
|
+
* @param {string} to - Selector for the end element.
|
|
22
|
+
* @returns {DocumentFragment} The extracted content.
|
|
23
|
+
*/
|
|
24
|
+
function getRangeContent(from, to) {
|
|
25
|
+
const startEl = document.querySelector(from);
|
|
26
|
+
const endEl = document.querySelector(to);
|
|
27
|
+
|
|
28
|
+
if (!startEl || !endEl) {
|
|
29
|
+
throw new Error(`Invalid selectors provided: from '${from}', to '${to}'`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const range = document.createRange();
|
|
33
|
+
range.setStartBefore(startEl);
|
|
34
|
+
range.setEndAfter(endEl);
|
|
35
|
+
|
|
36
|
+
return range.cloneContents();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a hidden container to render the content for PDF generation.
|
|
41
|
+
* @param {string} width - Width of the container.
|
|
42
|
+
* @returns {HTMLElement} The created container.
|
|
43
|
+
*/
|
|
44
|
+
function createHiddenContainer(width = "794px") {
|
|
45
|
+
const container = document.createElement("div");
|
|
46
|
+
Object.assign(container.style, {
|
|
47
|
+
position: "fixed",
|
|
48
|
+
left: "-9999px",
|
|
49
|
+
top: "0",
|
|
50
|
+
width: width,
|
|
51
|
+
background: "white",
|
|
52
|
+
padding: "20px",
|
|
53
|
+
boxSizing: "border-box", // Ensure padding doesn't affect width
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
document.body.appendChild(container);
|
|
57
|
+
return container;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts OKLCH colors to RGBA for canvas compatibility.
|
|
62
|
+
* @param {string} color - The color string.
|
|
63
|
+
* @param {CanvasRenderingContext2D} ctx - A canvas context for color processing.
|
|
64
|
+
* @returns {string} The collected RGBA color.
|
|
65
|
+
*/
|
|
66
|
+
function forceRgb(color, ctx) {
|
|
67
|
+
if (!color || typeof color !== "string" || !color.includes("oklch")) {
|
|
68
|
+
return color;
|
|
69
|
+
}
|
|
70
|
+
return color.replace(/oklch\([^)]+\)/g, (match) => {
|
|
71
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
72
|
+
ctx.fillStyle = match;
|
|
73
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
74
|
+
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
75
|
+
// data[3] is alpha in 0-255 range, rgba needs 0-1
|
|
76
|
+
return `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Processes an element and its children to convert OKLCH colors to RGB.
|
|
82
|
+
* @param {HTMLElement} element - The root element to process.
|
|
83
|
+
*/
|
|
84
|
+
function processOklchColors(element) {
|
|
85
|
+
const canvas = document.createElement("canvas");
|
|
86
|
+
canvas.width = 1;
|
|
87
|
+
canvas.height = 1;
|
|
88
|
+
const ctx = canvas.getContext("2d");
|
|
89
|
+
|
|
90
|
+
const elements = [element, ...element.getElementsByTagName("*")];
|
|
91
|
+
|
|
92
|
+
for (const el of elements) {
|
|
93
|
+
const style = window.getComputedStyle(el);
|
|
94
|
+
|
|
95
|
+
// Iterating over all properties
|
|
96
|
+
for (let i = 0; i < style.length; i++) {
|
|
97
|
+
const prop = style[i];
|
|
98
|
+
const val = style.getPropertyValue(prop);
|
|
99
|
+
if (val && val.includes("oklch")) {
|
|
100
|
+
el.style.setProperty(
|
|
101
|
+
prop,
|
|
102
|
+
forceRgb(val, ctx),
|
|
103
|
+
style.getPropertyPriority(prop)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle specific shorthand/computed properties aggressively
|
|
109
|
+
const extraProps = ["backgroundImage", "boxShadow", "fill", "stroke", "color", "borderColor", "backgroundColor"];
|
|
110
|
+
for (const prop of extraProps) {
|
|
111
|
+
const val = style[prop];
|
|
112
|
+
// Computed style access for camelCase properties
|
|
113
|
+
if (val && typeof val === 'string' && val.includes("oklch")) {
|
|
114
|
+
// We convert camelCase to kebab-case for setProperty if needed, but direct style assignment works too
|
|
115
|
+
// Using strict style assignment for these specific overrides
|
|
116
|
+
el.style[prop] = forceRgb(val, ctx);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generates a PDF from specified DOM elements.
|
|
125
|
+
* @param {Object} config - Configuration options.
|
|
126
|
+
* @param {Array<{from: string, to: string}>} config.pages - Array of page definitions with selectors.
|
|
127
|
+
* @param {string} [config.filename="document.pdf"] - Output filename.
|
|
128
|
+
* @param {Object} [config.jsPDFOptions] - Options for jsPDF constructor.
|
|
129
|
+
* @param {Object} [config.html2canvasOptions] - Options for html2canvas.
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
export async function generatePDF(config) {
|
|
133
|
+
validateConfig(config);
|
|
134
|
+
|
|
135
|
+
const {
|
|
136
|
+
pages,
|
|
137
|
+
filename = "document.pdf",
|
|
138
|
+
jsPDFOptions = { orientation: "p", unit: "mm", format: "a4" },
|
|
139
|
+
html2canvasOptions = {}
|
|
140
|
+
} = config;
|
|
141
|
+
|
|
142
|
+
const pdf = new jsPDF(jsPDFOptions);
|
|
143
|
+
let isFirstPage = true;
|
|
144
|
+
|
|
145
|
+
// Defaults for A4 size in mm roughly map to pixels at 96 DPI
|
|
146
|
+
const pageWidthPx = 794;
|
|
147
|
+
|
|
148
|
+
for (const page of pages) {
|
|
149
|
+
const { from, to } = page;
|
|
150
|
+
|
|
151
|
+
let container;
|
|
152
|
+
try {
|
|
153
|
+
const content = getRangeContent(from, to);
|
|
154
|
+
container = createHiddenContainer(`${pageWidthPx}px`);
|
|
155
|
+
container.appendChild(content);
|
|
156
|
+
|
|
157
|
+
// Mutate the DOM in the hidden container to fix OKLCH colors
|
|
158
|
+
processOklchColors(container);
|
|
159
|
+
|
|
160
|
+
const canvas = await html2canvas(container, {
|
|
161
|
+
scale: 2, // Higher scale for better resolution
|
|
162
|
+
useCORS: true,
|
|
163
|
+
windowWidth: pageWidthPx,
|
|
164
|
+
onclone: (clonedDoc) => {
|
|
165
|
+
// Re-run color processing on the cloned document used by html2canvas
|
|
166
|
+
const clonedContainer = clonedDoc.body.lastChild;
|
|
167
|
+
if (clonedContainer) {
|
|
168
|
+
processOklchColors(clonedContainer);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
...html2canvasOptions,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const imgData = canvas.toDataURL("image/png");
|
|
175
|
+
const pdfWidth = pdf.internal.pageSize.getWidth();
|
|
176
|
+
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
|
|
177
|
+
|
|
178
|
+
if (!isFirstPage) {
|
|
179
|
+
pdf.addPage();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight);
|
|
183
|
+
isFirstPage = false;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`Error processing page with selectors ${from} -> ${to}:`, error);
|
|
186
|
+
// Continue to next page or re-throw based on preference?
|
|
187
|
+
// For now, we log and continue to try to render partial document,
|
|
188
|
+
// but strictly speaking we might want to reject.
|
|
189
|
+
// Let's rethrow to inform the user the PDF generation failed.
|
|
190
|
+
if (container && container.parentNode) {
|
|
191
|
+
document.body.removeChild(container);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (container && container.parentNode) {
|
|
197
|
+
document.body.removeChild(container);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pdf.save(filename);
|
|
202
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { generatePDF } from "./generatePDF";
|