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 ADDED
@@ -0,0 +1,208 @@
1
+ # Smart PDF
2
+
3
+ [![npm version](https://img.shields.io/npm/v/smart-pdf.svg)](https://www.npmjs.com/package/smart-pdf)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Downloads](https://img.shields.io/npm/dt/smart-pdf.svg)](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
@@ -0,0 +1,6 @@
1
+ export interface PDFConfig {
2
+ pages: { from: string; to: string }[];
3
+ filename?: string;
4
+ }
5
+
6
+ export function generatePDF(config: PDFConfig): Promise<void>;
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";