react-web-pdf-editor 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 react-pdf-editor contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,438 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # react-web-pdf-editor
2
+
3
+ View and edit PDFs in the browser with React. Open a file, annotate it, change the page order, and export a new PDF. Rendering uses [PDF.js](https://mozilla.github.io/pdf.js/). Export uses [pdf-lib](https://pdf-lib.js.org/).
4
+
5
+ The editor includes:
6
+
7
+ - Continuous page viewing, zoom, and in-document search
8
+ - Text boxes, freehand ink, highlights, images, and signatures
9
+ - A properties panel for the selected object
10
+ - Page thumbnails, blank pages, delete, and drag-to-reorder
11
+ - Undo and redo
12
+ - Export that flattens your edits into a downloadable PDF
13
+
14
+ Annotations stay in memory until you export. Changing the `file` prop starts a fresh document and clears them.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install react-web-pdf-editor
20
+ ```
21
+
22
+ Peer dependencies: `react` and `react-dom` 18 or later.
23
+
24
+ Import the stylesheet once. Without it, the toolbar, pages, and page canvas have no layout.
25
+
26
+ ```tsx
27
+ import { PdfEditor } from "react-web-pdf-editor";
28
+ import "react-web-pdf-editor/styles.css";
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```tsx
34
+ import { PdfEditor } from "react-web-pdf-editor";
35
+ import "react-web-pdf-editor/styles.css";
36
+
37
+ export function App() {
38
+ return <PdfEditor file="/contract.pdf" name="Service agreement" />;
39
+ }
40
+ ```
41
+
42
+ `file` accepts a URL string, `URL`, `File`, `Blob`, `ArrayBuffer`, or `Uint8Array`. Leave it empty to show the empty state.
43
+
44
+ ### Open a file from the user's computer
45
+
46
+ ```tsx
47
+ import { useState } from "react";
48
+ import { PdfEditor, type PdfSource } from "react-web-pdf-editor";
49
+ import "react-web-pdf-editor/styles.css";
50
+
51
+ export function App() {
52
+ const [file, setFile] = useState<PdfSource | null>(null);
53
+
54
+ return (
55
+ <div style={{ height: "100vh" }}>
56
+ <input
57
+ type="file"
58
+ accept="application/pdf"
59
+ onChange={(event) => {
60
+ const next = event.target.files?.[0];
61
+ if (next) {
62
+ setFile(next);
63
+ }
64
+ }}
65
+ />
66
+ <PdfEditor file={file} style={{ height: "calc(100vh - 40px)" }} />
67
+ </div>
68
+ );
69
+ }
70
+ ```
71
+
72
+ Give the editor a height. The root fills its parent (`height: 100%`), so a parent with no height collapses the canvas.
73
+
74
+ Remote URLs are fetched in the browser, with a download progress state when the server sends `Content-Length`. The PDF host must allow your origin through CORS. A `File` from an `<input>` does not have that restriction.
75
+
76
+ ## Using the editor
77
+
78
+ The built-in chrome has three regions: a toolbar, a page-thumbnail sidebar, and the document. On widths under 900px it switches to a compact layout: a slim top bar, a bottom tool dock, pages in a left drawer, and properties in a sheet.
79
+
80
+ ### Tools
81
+
82
+ | Tool | What it does |
83
+ | --- | --- |
84
+ | Select | Move, resize, and rotate text, ink, highlights, images, and signatures. Click empty canvas to clear the selection. |
85
+ | Text | Click the page to place a box, then type. Double-click an existing box to edit it. |
86
+ | Draw | Draw freehand ink. Pick a color and a width (thin, pen, marker, bold) in the properties panel. |
87
+ | Highlight | Drag across existing PDF text to mark those lines, or drag on a blank area to paint a rectangle. |
88
+ | Signature | Opens a dialog to draw, type, or upload a signature, then click the page to place it. |
89
+ | Image | Opens a dialog to choose a PNG, JPG, or WebP, then click the page to place it. |
90
+
91
+ Select an object to edit it in the properties panel:
92
+
93
+ - **Text:** content, font, size, color, bold, italic, alignment, rotation
94
+ - **Ink:** color and stroke width
95
+ - **Highlight:** color
96
+ - **Image:** opacity, rotation, and replace
97
+ - **Signature:** rotation
98
+
99
+ Text fonts in the panel are Arial, Helvetica, Times New Roman, Georgia, Courier New, and Verdana. On export, text is painted as it appears on screen so the saved PDF matches the editor.
100
+
101
+ Images are limited to 8 MB and are scaled so the longest edge is at most 2400 pixels. Signatures can be drawn, typed, or uploaded (PNG, JPG, or WebP, up to 4 MB). Typed signatures use script fonts loaded from Google Fonts, so a strict Content-Security-Policy that blocks `fonts.googleapis.com` will fall back to a plainer face. The dialog keeps up to five recent signatures in `localStorage` on this browser.
102
+
103
+ Press Delete or Backspace to remove the selected object. Press Escape to leave the current tool, close a compact drawer, or clear the selection.
104
+
105
+ ### Pages
106
+
107
+ The sidebar shows a thumbnail for every page, including your annotations.
108
+
109
+ - Click a thumbnail to jump to that page.
110
+ - Use the + control to insert a blank page after the current page.
111
+ - Delete a page from its thumbnail. The last remaining page cannot be deleted.
112
+ - Drag a thumbnail to reorder. With a thumbnail focused, Alt+Arrow also moves it.
113
+
114
+ Page insert, delete, and reorder rewrite the working PDF. They are disabled while a page change is still saving.
115
+
116
+ ### Find, zoom, and rename
117
+
118
+ Search is in the toolbar. Ctrl+F or Cmd+F focuses it. Enter jumps to the next match, Shift+Enter to the previous one, and Escape clears it. Matches scroll into view.
119
+
120
+ Zoom from the toolbar, with Ctrl or Cmd plus `+` / `-`, or with Ctrl or Cmd and the mouse wheel while the pointer is over the editor. The default range is 50% to 300%, starting at 100%.
121
+
122
+ The document name in the toolbar is editable. Click it, type a new name, and press Enter. That name is the default export filename. Characters that are illegal in file names (`/ \ ? % * : | " < >`) are stripped.
123
+
124
+ ### Export
125
+
126
+ The toolbar download button builds a PDF and saves it. The file includes the current page order and every annotation flattened onto the pages. If nothing has been annotated, export returns the current PDF bytes, including any page changes.
127
+
128
+ The download name is the document name, with `.pdf` added when it is missing. Set `exportFileName` when the download name should differ from the name shown in the toolbar.
129
+
130
+ Export runs in a worker and times out after 60 seconds. If the worker cannot start, export continues on the main thread.
131
+
132
+ ### Keyboard shortcuts
133
+
134
+ These apply while focus is inside the editor and you are not typing in a text field or dialog.
135
+
136
+ | Shortcut | Action |
137
+ | --- | --- |
138
+ | Ctrl+Z / Cmd+Z | Undo |
139
+ | Ctrl+Y or Ctrl+Shift+Z / Cmd+Shift+Z | Redo |
140
+ | Ctrl+F / Cmd+F | Find |
141
+ | Ctrl or Cmd + `+` / `-` | Zoom in / out |
142
+ | Ctrl or Cmd + mouse wheel | Zoom |
143
+ | Delete / Backspace | Delete the selected object |
144
+ | Escape | Close a drawer, stop editing, or leave the current tool |
145
+ | Left / Right | Previous / next page |
146
+ | Up / Down | Scroll the page |
147
+ | Page Up / Page Down | Scroll about one screen |
148
+ | Home / End | Jump to the top or bottom of the document |
149
+
150
+ Undo covers annotation edits, page changes, and renames.
151
+
152
+ ## Props
153
+
154
+ | Prop | Type | Default | Description |
155
+ | --- | --- | --- | --- |
156
+ | `file` | `PdfSource \| null` | — | PDF to open. Omit it, or pass `null`, to show the empty state. |
157
+ | `name` | `string` | File name, or `"document.pdf"` | Document name in the toolbar. When this prop changes, the editor follows it. |
158
+ | `onNameChange` | `(name: string) => void` | — | Called after a rename in the toolbar or an undo/redo that restores a name. |
159
+ | `className` | `string` | — | Extra class on the editor root. |
160
+ | `style` | `CSSProperties` | — | Inline style on the editor root. Use this to set height. |
161
+ | `page` | `number` | — | Controlled page number, starting at 1. |
162
+ | `defaultPage` | `number` | `1` | Initial page when `page` is not set. |
163
+ | `onPageChange` | `(page: number) => void` | — | Called when the visible page changes, including from scrolling. |
164
+ | `scale` | `number` | — | Controlled zoom. `1` is 100%. |
165
+ | `defaultScale` | `number` | `1` | Initial zoom when `scale` is not set. |
166
+ | `minScale` | `number` | `0.5` | Smallest zoom. |
167
+ | `maxScale` | `number` | `3` | Largest zoom. |
168
+ | `onScaleChange` | `(scale: number) => void` | — | Called when the zoom changes. |
169
+ | `showToolbar` | `boolean` | `true` | `false` hides the toolbar even if `toolbar` is set. |
170
+ | `toolbar` | `(api: ToolbarApi) => ReactNode` | Built-in toolbar | Render your own toolbar. Omit it to keep the default. |
171
+ | `showSidebar` | `boolean` | `true` | `false` hides the pages sidebar even if `sidebar` is set. |
172
+ | `sidebar` | `(api: SidebarApi) => ReactNode` | Built-in sidebar | Render your own pages sidebar. Omit it to keep the default. |
173
+ | `emptyState` | `ReactNode` | `"Open a PDF to get started."` | Shown when `file` is empty. |
174
+ | `workerSrc` | `string` | PDF.js worker on unpkg | URL of the PDF.js worker used to render pages. |
175
+ | `exportWorkerSrc` | `string` | Bundled export worker | URL of a custom export worker. |
176
+ | `exportFileName` | `string` | Document name | Download filename. `.pdf` is appended when missing. |
177
+ | `onExport` | `(blob: Blob) => void \| Promise<void>` | Browser download | Receive the exported PDF yourself. When this is set, the editor does not download the file. |
178
+ | `onExportError` | `(error: Error) => void` | — | Called when export fails. The toolbar also exposes `exportError`. |
179
+ | `onLoadSuccess` | `({ numPages }) => void` | — | Called after the PDF has loaded. |
180
+ | `onLoadError` | `(error: Error) => void` | — | Called when the PDF cannot be loaded. |
181
+
182
+ `PdfSource` is `string | URL | File | Blob | ArrayBuffer | Uint8Array`.
183
+
184
+ ### Keep the page and zoom in your own state
185
+
186
+ Omit `page` and `scale` to let the editor own them. Pass them, and update them from the callbacks, to control them:
187
+
188
+ ```tsx
189
+ const [page, setPage] = useState(1);
190
+ const [scale, setScale] = useState(1);
191
+
192
+ <PdfEditor
193
+ file={file}
194
+ page={page}
195
+ scale={scale}
196
+ onPageChange={setPage}
197
+ onScaleChange={setScale}
198
+ />
199
+ ```
200
+
201
+ Do the same for the document name if a parent must stay in sync:
202
+
203
+ ```tsx
204
+ const [name, setName] = useState("Contract");
205
+
206
+ <PdfEditor file={file} name={name} onNameChange={setName} />
207
+ ```
208
+
209
+ ### Save the export yourself
210
+
211
+ ```tsx
212
+ <PdfEditor
213
+ file={file}
214
+ exportFileName="signed-contract.pdf"
215
+ onExport={async (blob) => {
216
+ const body = new FormData();
217
+ body.append("file", blob, "signed-contract.pdf");
218
+ await fetch("/api/documents", { method: "POST", body });
219
+ }}
220
+ onExportError={(error) => {
221
+ console.error(error);
222
+ }}
223
+ />
224
+ ```
225
+
226
+ `onExport` replaces the automatic download. Trigger a download yourself if you still want one:
227
+
228
+ ```tsx
229
+ onExport={(blob) => {
230
+ const url = URL.createObjectURL(blob);
231
+ const anchor = document.createElement("a");
232
+ anchor.href = url;
233
+ anchor.download = "signed-contract.pdf";
234
+ anchor.click();
235
+ URL.revokeObjectURL(url);
236
+ }}
237
+ ```
238
+
239
+ ## Custom toolbar
240
+
241
+ `toolbar` receives a `ToolbarApi`. `showToolbar={false}` removes the bar entirely.
242
+
243
+ ```tsx
244
+ import { PdfEditor, Toolbar } from "react-web-pdf-editor";
245
+
246
+ <PdfEditor
247
+ file={file}
248
+ toolbar={(api) => (
249
+ <header>
250
+ <span>{api.name}</span>
251
+ <button type="button" onClick={api.undo} disabled={!api.canUndo}>
252
+ Undo
253
+ </button>
254
+ <button type="button" onClick={api.redo} disabled={!api.canRedo}>
255
+ Redo
256
+ </button>
257
+ <button type="button" onClick={() => api.setTool("text")}>
258
+ Text
259
+ </button>
260
+ <button type="button" onClick={api.zoomOut} disabled={api.scale <= api.minScale}>
261
+ −
262
+ </button>
263
+ <span>{Math.round(api.scale * 100)}%</span>
264
+ <button type="button" onClick={api.zoomIn} disabled={api.scale >= api.maxScale}>
265
+ +
266
+ </button>
267
+ <span>
268
+ {api.page} / {api.numPages}
269
+ </span>
270
+ <button type="button" onClick={api.exportPdf} disabled={api.disabled || api.exporting}>
271
+ {api.exporting ? "Saving…" : "Save"}
272
+ </button>
273
+ </header>
274
+ )}
275
+ />
276
+ ```
277
+
278
+ Render the exported `Toolbar` when you want the default bar inside your own layout:
279
+
280
+ ```tsx
281
+ <PdfEditor file={file} toolbar={(api) => <Toolbar {...api} />} />
282
+ ```
283
+
284
+ `ToolbarApi` fields:
285
+
286
+ | Field | Description |
287
+ | --- | --- |
288
+ | `name`, `setName` | Document name. `setName` goes through the same rename path as the built-in field. |
289
+ | `disabled` | `true` while no PDF is loaded. |
290
+ | `canUndo`, `canRedo`, `undo`, `redo` | History. `undoLabel` and `redoLabel` describe the next step when present. |
291
+ | `shortcuts` | Platform undo/redo labels: `{ undo, redo, undoKeys, redoKeys }`. |
292
+ | `activeTool`, `setTool` | `"select"`, `"draw"`, `"text"`, `"highlight"`, `"image"`, or `"signature"`. |
293
+ | `scale`, `minScale`, `maxScale`, `setScale`, `zoomIn`, `zoomOut` | Zoom. |
294
+ | `page`, `numPages`, `setPage`, `prevPage`, `nextPage` | Page navigation. |
295
+ | `search` | `{ query, setQuery, matchCount, currentIndex, next, prev, clear }`. `currentIndex` is `-1` when there is no active match. |
296
+ | `exportPdf`, `exporting`, `exportError` | Start an export, and read its pending or failed state. |
297
+
298
+ ## Custom sidebar
299
+
300
+ `sidebar` receives a `SidebarApi`. `showSidebar={false}` removes the sidebar entirely.
301
+
302
+ ```tsx
303
+ import { PdfEditor, PagesSidebar } from "react-web-pdf-editor";
304
+
305
+ <PdfEditor
306
+ file={file}
307
+ sidebar={(api) => (
308
+ <aside>
309
+ {api.pages.map((page) => (
310
+ <button
311
+ key={page.pageNumber}
312
+ type="button"
313
+ aria-current={page.selected ? "page" : undefined}
314
+ onClick={() => api.setPage(page.pageNumber)}
315
+ >
316
+ Page {page.label}
317
+ </button>
318
+ ))}
319
+ <button
320
+ type="button"
321
+ onClick={() => api.addBlankPage(api.page)}
322
+ disabled={!api.canAdd || api.mutating}
323
+ >
324
+ Add page
325
+ </button>
326
+ </aside>
327
+ )}
328
+ />
329
+ ```
330
+
331
+ Keep the default thumbnails by rendering `PagesSidebar`:
332
+
333
+ ```tsx
334
+ <PdfEditor file={file} sidebar={(api) => <PagesSidebar {...api} />} />
335
+ ```
336
+
337
+ `SidebarApi` fields:
338
+
339
+ | Field | Description |
340
+ | --- | --- |
341
+ | `pdf` | The PDF.js document, or `null` while loading. |
342
+ | `page`, `numPages`, `setPage` | Visible page and navigation. |
343
+ | `pages` | Visual order. Each item has `pageNumber`, `index`, `label`, `selected`, and `justAdded`. |
344
+ | `addBlankPage(afterPage)` | Insert a blank page after a 1-based page number. |
345
+ | `deletePage(pageNumber)` | Remove a page. No effect when only one page remains. |
346
+ | `reorderPages(from, to)` | Move a page. `from` and `to` are 1-based positions in the visual order. |
347
+ | `disabled` | `true` while no PDF is loaded. |
348
+ | `mutating` | `true` while a page insert, delete, or reorder is saving. |
349
+ | `adding` | `true` while a blank page is being inserted. |
350
+ | `canAdd`, `canDelete`, `canReorder` | Whether that page action is currently allowed. |
351
+ | `error` | Message from the last failed page change, or `null`. |
352
+ | `drawStrokes`, `highlights`, `textAnnotations`, `signatures`, `images` | Current annotations, used by the default thumbnails. |
353
+
354
+ `pageNumber` is the page's id in the loaded PDF. `label` is its position in the sidebar, which changes after a reorder.
355
+
356
+ ## Workers
357
+
358
+ PDF.js renders pages on a worker. The default worker URL is a version-matched file on unpkg:
359
+
360
+ ```text
361
+ https://unpkg.com/pdfjs-dist@<version>/build/pdf.worker.min.mjs
362
+ ```
363
+
364
+ Self-host that file and pass `workerSrc` when the CDN is blocked or you want the worker on your own origin. Copy it from `node_modules/pdfjs-dist/build/pdf.worker.min.mjs`.
365
+
366
+ ```tsx
367
+ <PdfEditor file={file} workerSrc="/pdf.worker.min.mjs" />
368
+ ```
369
+
370
+ You can also set it once, before the editor mounts:
371
+
372
+ ```tsx
373
+ import { configurePdfWorker } from "react-web-pdf-editor";
374
+
375
+ configurePdfWorker("/pdf.worker.min.mjs");
376
+ ```
377
+
378
+ Export uses a second worker shipped with this package. Vite, Webpack, and other bundlers that understand `new URL(..., import.meta.url)` resolve it automatically. Pass `exportWorkerSrc` only if you need to host that worker at a specific URL. A failed worker start falls back to exporting on the main thread.
379
+
380
+ ## Next.js
381
+
382
+ Load the editor in the browser. The package talks to workers, canvas, and `window` during render.
383
+
384
+ ```tsx
385
+ "use client";
386
+
387
+ import dynamic from "next/dynamic";
388
+ import "react-web-pdf-editor/styles.css";
389
+
390
+ const PdfEditor = dynamic(
391
+ () => import("react-web-pdf-editor").then((mod) => mod.PdfEditor),
392
+ { ssr: false },
393
+ );
394
+
395
+ export function DocumentEditor() {
396
+ return <PdfEditor file="/contract.pdf" style={{ height: "100vh" }} />;
397
+ }
398
+ ```
399
+
400
+ ## Styling
401
+
402
+ Import `react-web-pdf-editor/styles.css` once at the app root. Classes are prefixed with `rpe-`. Pass `className` and `style` to the root when you need to size or position the editor:
403
+
404
+ ```tsx
405
+ <PdfEditor
406
+ file={file}
407
+ className="contract-editor"
408
+ style={{ height: "100vh", width: "100%" }}
409
+ />
410
+ ```
411
+
412
+ The editor fills the height you give it. Below 900px of editor width, that same layout uses the compact drawers described above. Desktop layout is unchanged above that width.
413
+
414
+ ## Exported types
415
+
416
+ These types are exported for custom toolbars, sidebars, and annotation handling:
417
+
418
+ `PdfEditorProps`, `PdfSource`, `PdfLoadSuccess`, `Tool`, `ToolbarApi`, `ToolbarSearchState`, `ToolbarShortcuts`, `SidebarApi`, `SidebarPage`, `TextAnnotation`, `DrawStroke`, `DrawPoint`, `HighlightAnnotation`, `HighlightRect`, `ImageAnnotation`, `PendingImage`, `SignatureAnnotation`, `PendingSignature`.
419
+
420
+ ## Local development
421
+
422
+ ```bash
423
+ npm install
424
+ npm run dev
425
+ ```
426
+
427
+ The playground runs at `http://localhost:3000` and loads the library from `src/`. If port 3000 is already in use, the server does not start.
428
+
429
+ ```bash
430
+ npm run build
431
+ npm run typecheck
432
+ ```
433
+
434
+ `npm run build` writes ESM, CommonJS, and TypeScript declarations to `dist/`, and copies `styles.css` there. `prepublishOnly` runs that build before `npm publish`.
435
+
436
+ ## License
437
+
438
+ MIT