pdfjs-reader-core 0.1.0 → 0.1.2
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 +870 -0
- package/dist/index.cjs +3442 -582
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +745 -2
- package/dist/index.d.ts +745 -2
- package/dist/index.js +3388 -561
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
# pdfjs-reader-core
|
|
2
|
+
|
|
3
|
+
A React library for rendering PDFs with built-in search, highlighting, and annotation capabilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install pdfjs-reader-core
|
|
9
|
+
# or
|
|
10
|
+
yarn add pdfjs-reader-core
|
|
11
|
+
# or
|
|
12
|
+
pnpm add pdfjs-reader-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Rendering PDFs
|
|
18
|
+
|
|
19
|
+
### Quick Start - Full-Featured Viewer
|
|
20
|
+
|
|
21
|
+
The easiest way to render a PDF with all features enabled:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { PDFViewerClient } from 'pdfjs-reader-core';
|
|
25
|
+
import 'pdfjs-reader-core/styles.css';
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<div style={{ height: '100vh' }}>
|
|
30
|
+
<PDFViewerClient
|
|
31
|
+
src="/document.pdf"
|
|
32
|
+
showToolbar
|
|
33
|
+
showSidebar
|
|
34
|
+
onDocumentLoad={({ numPages }) => console.log(`Loaded ${numPages} pages`)}
|
|
35
|
+
onError={(error) => console.error('Failed to load:', error)}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Custom Viewer with Hooks
|
|
43
|
+
|
|
44
|
+
For more control, use the provider and hooks:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import {
|
|
48
|
+
PDFViewerProvider,
|
|
49
|
+
usePDFViewer,
|
|
50
|
+
ContinuousScrollContainer,
|
|
51
|
+
Toolbar,
|
|
52
|
+
Sidebar,
|
|
53
|
+
} from 'pdfjs-reader-core';
|
|
54
|
+
import 'pdfjs-reader-core/styles.css';
|
|
55
|
+
|
|
56
|
+
function App() {
|
|
57
|
+
return (
|
|
58
|
+
<PDFViewerProvider>
|
|
59
|
+
<MyPDFViewer />
|
|
60
|
+
</PDFViewerProvider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function MyPDFViewer() {
|
|
65
|
+
const { loadDocument, isLoading, error, numPages } = usePDFViewer();
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
loadDocument({ src: '/document.pdf' });
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
if (isLoading) return <div>Loading...</div>;
|
|
72
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
76
|
+
<Sidebar />
|
|
77
|
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
78
|
+
<Toolbar />
|
|
79
|
+
<ContinuousScrollContainer />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Load PDF from Different Sources
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const { loadDocument } = usePDFViewer();
|
|
90
|
+
|
|
91
|
+
// From URL
|
|
92
|
+
await loadDocument({ src: 'https://example.com/document.pdf' });
|
|
93
|
+
|
|
94
|
+
// From file input
|
|
95
|
+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
96
|
+
const file = e.target.files?.[0];
|
|
97
|
+
if (file) {
|
|
98
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
99
|
+
await loadDocument({ src: arrayBuffer });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// From base64
|
|
104
|
+
const base64 = 'JVBERi0xLjQK...';
|
|
105
|
+
const binaryString = atob(base64);
|
|
106
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
107
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
108
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
109
|
+
}
|
|
110
|
+
await loadDocument({ src: bytes });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Navigation API
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const {
|
|
117
|
+
currentPage, // Current page number (1-indexed)
|
|
118
|
+
numPages, // Total pages
|
|
119
|
+
scale, // Current zoom level (1 = 100%)
|
|
120
|
+
goToPage, // Navigate to specific page
|
|
121
|
+
nextPage, // Go to next page
|
|
122
|
+
previousPage, // Go to previous page
|
|
123
|
+
setScale, // Set zoom level
|
|
124
|
+
zoomIn, // Zoom in by preset amount
|
|
125
|
+
zoomOut, // Zoom out by preset amount
|
|
126
|
+
fitToWidth, // Fit page to container width
|
|
127
|
+
fitToPage, // Fit entire page in view
|
|
128
|
+
rotateClockwise, // Rotate 90° clockwise
|
|
129
|
+
} = usePDFViewer();
|
|
130
|
+
|
|
131
|
+
// Examples
|
|
132
|
+
goToPage(5); // Go to page 5
|
|
133
|
+
setScale(1.5); // Set zoom to 150%
|
|
134
|
+
zoomIn(); // Zoom in
|
|
135
|
+
fitToWidth(); // Fit to width
|
|
136
|
+
rotateClockwise(); // Rotate
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 2. Search
|
|
142
|
+
|
|
143
|
+
Search text across all pages and navigate through results.
|
|
144
|
+
|
|
145
|
+
### Basic Search
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const {
|
|
149
|
+
search, // (query: string) => Promise<void>
|
|
150
|
+
searchResults, // Array of search results
|
|
151
|
+
currentSearchResult, // Index of current result
|
|
152
|
+
nextSearchResult, // Go to next result
|
|
153
|
+
previousSearchResult, // Go to previous result
|
|
154
|
+
clearSearch, // Clear search
|
|
155
|
+
goToPage, // Navigate to page
|
|
156
|
+
} = usePDFViewer();
|
|
157
|
+
|
|
158
|
+
// Perform search
|
|
159
|
+
await search('important term');
|
|
160
|
+
|
|
161
|
+
// Navigate results
|
|
162
|
+
console.log(`Found ${searchResults.length} matches`);
|
|
163
|
+
nextSearchResult(); // Go to next match
|
|
164
|
+
previousSearchResult(); // Go to previous match
|
|
165
|
+
|
|
166
|
+
// Clear when done
|
|
167
|
+
clearSearch();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Search Result Structure
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
interface SearchResult {
|
|
174
|
+
pageNumber: number; // Page where match was found
|
|
175
|
+
text: string; // Matched text
|
|
176
|
+
index: number; // Index in results array
|
|
177
|
+
rects?: { // Bounding rectangles for highlighting
|
|
178
|
+
x: number;
|
|
179
|
+
y: number;
|
|
180
|
+
width: number;
|
|
181
|
+
height: number;
|
|
182
|
+
}[];
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Complete Search UI Example
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useState } from 'react';
|
|
190
|
+
import { usePDFViewer } from 'pdfjs-reader-core';
|
|
191
|
+
|
|
192
|
+
function SearchBar() {
|
|
193
|
+
const [query, setQuery] = useState('');
|
|
194
|
+
const {
|
|
195
|
+
search,
|
|
196
|
+
searchResults,
|
|
197
|
+
currentSearchResult,
|
|
198
|
+
nextSearchResult,
|
|
199
|
+
previousSearchResult,
|
|
200
|
+
clearSearch,
|
|
201
|
+
} = usePDFViewer();
|
|
202
|
+
|
|
203
|
+
const handleSearch = async (text: string) => {
|
|
204
|
+
setQuery(text);
|
|
205
|
+
if (text.length >= 2) {
|
|
206
|
+
await search(text);
|
|
207
|
+
} else {
|
|
208
|
+
clearSearch();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div>
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
value={query}
|
|
217
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
218
|
+
placeholder="Search..."
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
{searchResults.length > 0 && (
|
|
222
|
+
<div>
|
|
223
|
+
<span>
|
|
224
|
+
{currentSearchResult + 1} of {searchResults.length}
|
|
225
|
+
</span>
|
|
226
|
+
<button onClick={previousSearchResult}>←</button>
|
|
227
|
+
<button onClick={nextSearchResult}>→</button>
|
|
228
|
+
<button onClick={clearSearch}>Clear</button>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 3. Highlighting
|
|
239
|
+
|
|
240
|
+
Create persistent highlights on PDF text. Highlights are rendered as colored overlays.
|
|
241
|
+
|
|
242
|
+
### Add Highlight Programmatically
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
const { addHighlight, highlights, removeHighlight } = usePDFViewer();
|
|
246
|
+
|
|
247
|
+
// Add a highlight with coordinates
|
|
248
|
+
const highlight = addHighlight({
|
|
249
|
+
pageNumber: 1,
|
|
250
|
+
text: 'The highlighted text',
|
|
251
|
+
color: 'yellow', // 'yellow' | 'green' | 'blue' | 'pink' | 'orange'
|
|
252
|
+
rects: [
|
|
253
|
+
{ x: 72, y: 100, width: 200, height: 14 },
|
|
254
|
+
{ x: 72, y: 116, width: 150, height: 14 }, // Multi-line support
|
|
255
|
+
],
|
|
256
|
+
comment: 'Optional note', // Optional
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log(highlight.id); // Unique ID for the highlight
|
|
260
|
+
|
|
261
|
+
// List all highlights
|
|
262
|
+
highlights.forEach(h => {
|
|
263
|
+
console.log(`Page ${h.pageNumber}: "${h.text}" (${h.color})`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Remove a highlight
|
|
267
|
+
removeHighlight(highlight.id);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Highlight from Search Results
|
|
271
|
+
|
|
272
|
+
Convert search results into permanent highlights:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
const { search, searchResults, addHighlight } = usePDFViewer();
|
|
276
|
+
|
|
277
|
+
// Search for a term
|
|
278
|
+
await search('important');
|
|
279
|
+
|
|
280
|
+
// Highlight all matches
|
|
281
|
+
searchResults.forEach((result) => {
|
|
282
|
+
if (result.rects && result.rects.length > 0) {
|
|
283
|
+
addHighlight({
|
|
284
|
+
pageNumber: result.pageNumber,
|
|
285
|
+
text: result.text,
|
|
286
|
+
rects: result.rects,
|
|
287
|
+
color: 'yellow',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Using the useHighlights Hook
|
|
294
|
+
|
|
295
|
+
For more control over highlights:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { useHighlights } from 'pdfjs-reader-core';
|
|
299
|
+
|
|
300
|
+
function HighlightManager() {
|
|
301
|
+
const {
|
|
302
|
+
allHighlights, // All highlights
|
|
303
|
+
highlightsForPage, // (pageNum) => highlights on that page
|
|
304
|
+
addHighlight, // Add new highlight
|
|
305
|
+
updateHighlight, // Update existing
|
|
306
|
+
deleteHighlight, // Delete by ID
|
|
307
|
+
selectedHighlight, // Currently selected highlight
|
|
308
|
+
selectHighlight, // Select a highlight
|
|
309
|
+
createHighlightFromSelection, // Create from text selection
|
|
310
|
+
} = useHighlights({
|
|
311
|
+
onHighlightCreate: (h) => console.log('Created:', h),
|
|
312
|
+
onHighlightUpdate: (h) => console.log('Updated:', h),
|
|
313
|
+
onHighlightDelete: (id) => console.log('Deleted:', id),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Get highlights for page 1
|
|
317
|
+
const page1Highlights = highlightsForPage(1);
|
|
318
|
+
|
|
319
|
+
// Update a highlight's color
|
|
320
|
+
updateHighlight('highlight-id', { color: 'green' });
|
|
321
|
+
|
|
322
|
+
// Add a comment to highlight
|
|
323
|
+
updateHighlight('highlight-id', { comment: 'This is important!' });
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div>
|
|
327
|
+
<h3>Highlights ({allHighlights.length})</h3>
|
|
328
|
+
{allHighlights.map(h => (
|
|
329
|
+
<div key={h.id} onClick={() => selectHighlight(h.id)}>
|
|
330
|
+
<span style={{ background: h.color }}>{h.text}</span>
|
|
331
|
+
<button onClick={() => deleteHighlight(h.id)}>Delete</button>
|
|
332
|
+
</div>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Highlight Type Definition
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
interface Highlight {
|
|
343
|
+
id: string;
|
|
344
|
+
pageNumber: number;
|
|
345
|
+
rects: { x: number; y: number; width: number; height: number }[];
|
|
346
|
+
text: string;
|
|
347
|
+
color: 'yellow' | 'green' | 'blue' | 'pink' | 'orange';
|
|
348
|
+
comment?: string;
|
|
349
|
+
source?: 'user' | 'agent'; // Who created it
|
|
350
|
+
createdAt: Date;
|
|
351
|
+
updatedAt: Date;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Persist Highlights
|
|
356
|
+
|
|
357
|
+
Save and restore highlights:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import {
|
|
361
|
+
saveHighlights,
|
|
362
|
+
loadHighlights,
|
|
363
|
+
exportHighlightsAsJSON,
|
|
364
|
+
importHighlightsFromJSON,
|
|
365
|
+
} from 'pdfjs-reader-core';
|
|
366
|
+
|
|
367
|
+
// Save to localStorage
|
|
368
|
+
saveHighlights('doc-123', highlights);
|
|
369
|
+
|
|
370
|
+
// Load from localStorage
|
|
371
|
+
const saved = loadHighlights('doc-123');
|
|
372
|
+
|
|
373
|
+
// Export as JSON file
|
|
374
|
+
exportHighlightsAsJSON(highlights, 'my-highlights.json');
|
|
375
|
+
|
|
376
|
+
// Import from JSON
|
|
377
|
+
const imported = await importHighlightsFromJSON(jsonFile);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 4. Annotations
|
|
383
|
+
|
|
384
|
+
Add notes, drawings, and shapes to PDFs.
|
|
385
|
+
|
|
386
|
+
### Add Sticky Notes
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
import { useAnnotationStore } from 'pdfjs-reader-core';
|
|
390
|
+
|
|
391
|
+
function NoteManager() {
|
|
392
|
+
const addNote = useAnnotationStore((s) => s.addNote);
|
|
393
|
+
const annotations = useAnnotationStore((s) => s.annotations);
|
|
394
|
+
const deleteAnnotation = useAnnotationStore((s) => s.deleteAnnotation);
|
|
395
|
+
|
|
396
|
+
// Add a note at specific position
|
|
397
|
+
const createNote = () => {
|
|
398
|
+
addNote({
|
|
399
|
+
pageNumber: 1,
|
|
400
|
+
x: 100, // X position in PDF points
|
|
401
|
+
y: 200, // Y position in PDF points
|
|
402
|
+
content: 'This is my note',
|
|
403
|
+
color: '#ffeb3b', // Note color
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// List all notes
|
|
408
|
+
const notes = annotations.filter(a => a.type === 'note');
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<div>
|
|
412
|
+
<button onClick={createNote}>Add Note</button>
|
|
413
|
+
{notes.map(note => (
|
|
414
|
+
<div key={note.id}>
|
|
415
|
+
Page {note.pageNumber}: {note.content}
|
|
416
|
+
<button onClick={() => deleteAnnotation(note.id)}>Delete</button>
|
|
417
|
+
</div>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Add Shapes
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
const addShape = useAnnotationStore((s) => s.addShape);
|
|
428
|
+
|
|
429
|
+
// Rectangle
|
|
430
|
+
addShape({
|
|
431
|
+
pageNumber: 1,
|
|
432
|
+
shapeType: 'rect',
|
|
433
|
+
x: 100,
|
|
434
|
+
y: 200,
|
|
435
|
+
width: 150,
|
|
436
|
+
height: 80,
|
|
437
|
+
color: '#ef4444',
|
|
438
|
+
strokeWidth: 2,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Circle
|
|
442
|
+
addShape({
|
|
443
|
+
pageNumber: 1,
|
|
444
|
+
shapeType: 'circle',
|
|
445
|
+
x: 300,
|
|
446
|
+
y: 200,
|
|
447
|
+
width: 100,
|
|
448
|
+
height: 100,
|
|
449
|
+
color: '#22c55e',
|
|
450
|
+
strokeWidth: 2,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Arrow
|
|
454
|
+
addShape({
|
|
455
|
+
pageNumber: 1,
|
|
456
|
+
shapeType: 'arrow',
|
|
457
|
+
x: 100,
|
|
458
|
+
y: 350,
|
|
459
|
+
width: 120,
|
|
460
|
+
height: 40,
|
|
461
|
+
color: '#3b82f6',
|
|
462
|
+
strokeWidth: 3,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Line
|
|
466
|
+
addShape({
|
|
467
|
+
pageNumber: 1,
|
|
468
|
+
shapeType: 'line',
|
|
469
|
+
x: 100,
|
|
470
|
+
y: 450,
|
|
471
|
+
width: 200,
|
|
472
|
+
height: 0,
|
|
473
|
+
color: '#000000',
|
|
474
|
+
strokeWidth: 2,
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Freehand Drawing
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
const startDrawing = useAnnotationStore((s) => s.startDrawing);
|
|
482
|
+
const addDrawingPoint = useAnnotationStore((s) => s.addDrawingPoint);
|
|
483
|
+
const finishDrawing = useAnnotationStore((s) => s.finishDrawing);
|
|
484
|
+
const setDrawingColor = useAnnotationStore((s) => s.setDrawingColor);
|
|
485
|
+
const setDrawingStrokeWidth = useAnnotationStore((s) => s.setDrawingStrokeWidth);
|
|
486
|
+
|
|
487
|
+
// Configure drawing
|
|
488
|
+
setDrawingColor('#ff0000');
|
|
489
|
+
setDrawingStrokeWidth(3);
|
|
490
|
+
|
|
491
|
+
// Start drawing on page 1 at position (100, 200)
|
|
492
|
+
startDrawing(1, { x: 100, y: 200 });
|
|
493
|
+
|
|
494
|
+
// Add points as user draws
|
|
495
|
+
addDrawingPoint({ x: 110, y: 210 });
|
|
496
|
+
addDrawingPoint({ x: 120, y: 205 });
|
|
497
|
+
addDrawingPoint({ x: 130, y: 215 });
|
|
498
|
+
|
|
499
|
+
// Finish drawing (saves the annotation)
|
|
500
|
+
finishDrawing();
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Enable Drawing Mode UI
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
const setActiveAnnotationTool = useAnnotationStore((s) => s.setActiveAnnotationTool);
|
|
507
|
+
const activeAnnotationTool = useAnnotationStore((s) => s.activeAnnotationTool);
|
|
508
|
+
|
|
509
|
+
// Enable drawing mode
|
|
510
|
+
setActiveAnnotationTool('draw');
|
|
511
|
+
|
|
512
|
+
// Enable note mode (click to add notes)
|
|
513
|
+
setActiveAnnotationTool('note');
|
|
514
|
+
|
|
515
|
+
// Enable shape mode
|
|
516
|
+
setActiveAnnotationTool('shape');
|
|
517
|
+
|
|
518
|
+
// Disable annotation mode
|
|
519
|
+
setActiveAnnotationTool(null);
|
|
520
|
+
|
|
521
|
+
// Check current mode
|
|
522
|
+
if (activeAnnotationTool === 'draw') {
|
|
523
|
+
console.log('Drawing mode is active');
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Annotation Type Definition
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
interface Annotation {
|
|
531
|
+
id: string;
|
|
532
|
+
pageNumber: number;
|
|
533
|
+
type: 'note' | 'drawing' | 'shape';
|
|
534
|
+
|
|
535
|
+
// For notes
|
|
536
|
+
content?: string;
|
|
537
|
+
x?: number;
|
|
538
|
+
y?: number;
|
|
539
|
+
|
|
540
|
+
// For shapes
|
|
541
|
+
shapeType?: 'rect' | 'circle' | 'arrow' | 'line';
|
|
542
|
+
width?: number;
|
|
543
|
+
height?: number;
|
|
544
|
+
|
|
545
|
+
// For drawings
|
|
546
|
+
points?: { x: number; y: number }[];
|
|
547
|
+
|
|
548
|
+
// Common
|
|
549
|
+
color: string;
|
|
550
|
+
strokeWidth?: number;
|
|
551
|
+
createdAt: Date;
|
|
552
|
+
updatedAt: Date;
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 5. Complete Example
|
|
559
|
+
|
|
560
|
+
Here's a full example combining rendering, search, highlighting, and annotations:
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
import { useState, useEffect } from 'react';
|
|
564
|
+
import {
|
|
565
|
+
PDFViewerProvider,
|
|
566
|
+
usePDFViewer,
|
|
567
|
+
useHighlights,
|
|
568
|
+
useAnnotationStore,
|
|
569
|
+
ContinuousScrollContainer,
|
|
570
|
+
} from 'pdfjs-reader-core';
|
|
571
|
+
import 'pdfjs-reader-core/styles.css';
|
|
572
|
+
|
|
573
|
+
function App() {
|
|
574
|
+
return (
|
|
575
|
+
<PDFViewerProvider>
|
|
576
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
577
|
+
<ControlPanel />
|
|
578
|
+
<div style={{ flex: 1 }}>
|
|
579
|
+
<ContinuousScrollContainer />
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
</PDFViewerProvider>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function ControlPanel() {
|
|
587
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
588
|
+
|
|
589
|
+
// PDF viewer controls
|
|
590
|
+
const {
|
|
591
|
+
loadDocument,
|
|
592
|
+
currentPage,
|
|
593
|
+
numPages,
|
|
594
|
+
goToPage,
|
|
595
|
+
search,
|
|
596
|
+
searchResults,
|
|
597
|
+
clearSearch,
|
|
598
|
+
} = usePDFViewer();
|
|
599
|
+
|
|
600
|
+
// Highlight controls
|
|
601
|
+
const { allHighlights, addHighlight, deleteHighlight } = useHighlights();
|
|
602
|
+
|
|
603
|
+
// Annotation controls
|
|
604
|
+
const addNote = useAnnotationStore((s) => s.addNote);
|
|
605
|
+
const annotations = useAnnotationStore((s) => s.annotations);
|
|
606
|
+
|
|
607
|
+
// Load PDF on mount
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
loadDocument({ src: '/sample.pdf' });
|
|
610
|
+
}, []);
|
|
611
|
+
|
|
612
|
+
// Search handler
|
|
613
|
+
const handleSearch = async () => {
|
|
614
|
+
if (searchQuery.length >= 2) {
|
|
615
|
+
await search(searchQuery);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Highlight all search results
|
|
620
|
+
const highlightSearchResults = () => {
|
|
621
|
+
searchResults.forEach((result) => {
|
|
622
|
+
if (result.rects?.length) {
|
|
623
|
+
addHighlight({
|
|
624
|
+
pageNumber: result.pageNumber,
|
|
625
|
+
text: result.text,
|
|
626
|
+
rects: result.rects,
|
|
627
|
+
color: 'yellow',
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
clearSearch();
|
|
632
|
+
setSearchQuery('');
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Add note at center of current page
|
|
636
|
+
const addNoteToCurrentPage = () => {
|
|
637
|
+
addNote({
|
|
638
|
+
pageNumber: currentPage,
|
|
639
|
+
x: 300,
|
|
640
|
+
y: 400,
|
|
641
|
+
content: 'New note',
|
|
642
|
+
color: '#ffeb3b',
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
<div style={{ width: 300, padding: 16, borderRight: '1px solid #ccc' }}>
|
|
648
|
+
{/* Navigation */}
|
|
649
|
+
<div>
|
|
650
|
+
<h3>Navigation</h3>
|
|
651
|
+
<button onClick={() => goToPage(currentPage - 1)}>Previous</button>
|
|
652
|
+
<span> Page {currentPage} of {numPages} </span>
|
|
653
|
+
<button onClick={() => goToPage(currentPage + 1)}>Next</button>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
{/* Search */}
|
|
657
|
+
<div>
|
|
658
|
+
<h3>Search</h3>
|
|
659
|
+
<input
|
|
660
|
+
value={searchQuery}
|
|
661
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
662
|
+
placeholder="Search..."
|
|
663
|
+
/>
|
|
664
|
+
<button onClick={handleSearch}>Search</button>
|
|
665
|
+
{searchResults.length > 0 && (
|
|
666
|
+
<div>
|
|
667
|
+
<p>Found {searchResults.length} matches</p>
|
|
668
|
+
<button onClick={highlightSearchResults}>
|
|
669
|
+
Highlight All
|
|
670
|
+
</button>
|
|
671
|
+
</div>
|
|
672
|
+
)}
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
{/* Highlights */}
|
|
676
|
+
<div>
|
|
677
|
+
<h3>Highlights ({allHighlights.length})</h3>
|
|
678
|
+
{allHighlights.map((h) => (
|
|
679
|
+
<div key={h.id}>
|
|
680
|
+
<span style={{ background: h.color }}>
|
|
681
|
+
Page {h.pageNumber}: {h.text.slice(0, 30)}...
|
|
682
|
+
</span>
|
|
683
|
+
<button onClick={() => deleteHighlight(h.id)}>×</button>
|
|
684
|
+
</div>
|
|
685
|
+
))}
|
|
686
|
+
</div>
|
|
687
|
+
|
|
688
|
+
{/* Annotations */}
|
|
689
|
+
<div>
|
|
690
|
+
<h3>Notes ({annotations.filter(a => a.type === 'note').length})</h3>
|
|
691
|
+
<button onClick={addNoteToCurrentPage}>Add Note</button>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export default App;
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## API Reference
|
|
703
|
+
|
|
704
|
+
### PDFViewerClient Props
|
|
705
|
+
|
|
706
|
+
| Prop | Type | Default | Description |
|
|
707
|
+
|------|------|---------|-------------|
|
|
708
|
+
| `src` | `string \| ArrayBuffer` | required | PDF source URL or data |
|
|
709
|
+
| `showToolbar` | `boolean` | `true` | Show the toolbar |
|
|
710
|
+
| `showSidebar` | `boolean` | `true` | Show the sidebar |
|
|
711
|
+
| `viewMode` | `'single' \| 'continuous' \| 'dual'` | `'continuous'` | Page view mode |
|
|
712
|
+
| `theme` | `'light' \| 'dark' \| 'sepia'` | `'light'` | Color theme |
|
|
713
|
+
| `initialPage` | `number` | `1` | Initial page to display |
|
|
714
|
+
| `initialScale` | `number` | `1` | Initial zoom scale |
|
|
715
|
+
| `onDocumentLoad` | `(event) => void` | - | Called when document loads |
|
|
716
|
+
| `onPageChange` | `(page) => void` | - | Called when page changes |
|
|
717
|
+
| `onError` | `(error) => void` | - | Called on error |
|
|
718
|
+
|
|
719
|
+
### usePDFViewer() Return Value
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
{
|
|
723
|
+
// Document
|
|
724
|
+
document: PDFDocumentProxy | null;
|
|
725
|
+
numPages: number;
|
|
726
|
+
isLoading: boolean;
|
|
727
|
+
error: Error | null;
|
|
728
|
+
loadDocument: (options: LoadOptions) => Promise<void>;
|
|
729
|
+
|
|
730
|
+
// Navigation
|
|
731
|
+
currentPage: number;
|
|
732
|
+
goToPage: (page: number) => void;
|
|
733
|
+
nextPage: () => void;
|
|
734
|
+
previousPage: () => void;
|
|
735
|
+
|
|
736
|
+
// Zoom
|
|
737
|
+
scale: number;
|
|
738
|
+
setScale: (scale: number) => void;
|
|
739
|
+
zoomIn: () => void;
|
|
740
|
+
zoomOut: () => void;
|
|
741
|
+
fitToWidth: () => void;
|
|
742
|
+
fitToPage: () => void;
|
|
743
|
+
|
|
744
|
+
// Rotation
|
|
745
|
+
rotation: number;
|
|
746
|
+
rotateClockwise: () => void;
|
|
747
|
+
rotateCounterClockwise: () => void;
|
|
748
|
+
|
|
749
|
+
// Theme
|
|
750
|
+
theme: 'light' | 'dark' | 'sepia';
|
|
751
|
+
setTheme: (theme: Theme) => void;
|
|
752
|
+
|
|
753
|
+
// View mode
|
|
754
|
+
viewMode: 'single' | 'continuous' | 'dual';
|
|
755
|
+
setViewMode: (mode: ViewMode) => void;
|
|
756
|
+
|
|
757
|
+
// Search
|
|
758
|
+
search: (query: string) => Promise<void>;
|
|
759
|
+
searchResults: SearchResult[];
|
|
760
|
+
currentSearchResult: number;
|
|
761
|
+
nextSearchResult: () => void;
|
|
762
|
+
previousSearchResult: () => void;
|
|
763
|
+
clearSearch: () => void;
|
|
764
|
+
|
|
765
|
+
// Highlights
|
|
766
|
+
highlights: Highlight[];
|
|
767
|
+
addHighlight: (params: AddHighlightParams) => Highlight;
|
|
768
|
+
removeHighlight: (id: string) => void;
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Coordinate System
|
|
773
|
+
|
|
774
|
+
PDF coordinates use **points** (1 point = 1/72 inch):
|
|
775
|
+
- Origin (0, 0) is at the **top-left** corner
|
|
776
|
+
- X increases to the right
|
|
777
|
+
- Y increases downward
|
|
778
|
+
- Standard US Letter: 612 × 792 points (8.5" × 11")
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
// Place element 1 inch from left, 2 inches from top
|
|
782
|
+
const x = 72; // 1 inch × 72 points/inch
|
|
783
|
+
const y = 144; // 2 inches × 72 points/inch
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## Additional Features
|
|
789
|
+
|
|
790
|
+
### Themes
|
|
791
|
+
|
|
792
|
+
```tsx
|
|
793
|
+
const { theme, setTheme } = usePDFViewer();
|
|
794
|
+
|
|
795
|
+
setTheme('light'); // Light background
|
|
796
|
+
setTheme('dark'); // Dark background
|
|
797
|
+
setTheme('sepia'); // Sepia/warm background
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### View Modes
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
const { viewMode, setViewMode } = usePDFViewer();
|
|
804
|
+
|
|
805
|
+
setViewMode('single'); // One page at a time
|
|
806
|
+
setViewMode('continuous'); // Scrollable pages (virtualized)
|
|
807
|
+
setViewMode('dual'); // Two pages side by side
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Document Outline
|
|
811
|
+
|
|
812
|
+
```tsx
|
|
813
|
+
import { getOutline } from 'pdfjs-reader-core';
|
|
814
|
+
|
|
815
|
+
const outline = await getOutline(document);
|
|
816
|
+
// Returns table of contents structure
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Export Annotations
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
import {
|
|
823
|
+
exportHighlightsAsJSON,
|
|
824
|
+
exportHighlightsAsMarkdown,
|
|
825
|
+
downloadAnnotationsAsMarkdown,
|
|
826
|
+
} from 'pdfjs-reader-core';
|
|
827
|
+
|
|
828
|
+
// Export highlights as JSON
|
|
829
|
+
exportHighlightsAsJSON(highlights, 'highlights.json');
|
|
830
|
+
|
|
831
|
+
// Export as readable Markdown
|
|
832
|
+
downloadAnnotationsAsMarkdown({
|
|
833
|
+
highlights,
|
|
834
|
+
documentTitle: 'My Document',
|
|
835
|
+
}, 'notes.md');
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Performance
|
|
841
|
+
|
|
842
|
+
The library is optimized for fast rendering:
|
|
843
|
+
|
|
844
|
+
- **Virtualization** - Only visible pages are rendered
|
|
845
|
+
- **Range requests** - Downloads only needed PDF data
|
|
846
|
+
- **Page caching** - Loaded pages are cached
|
|
847
|
+
- **Full quality** - Renders at device pixel ratio for crisp text
|
|
848
|
+
|
|
849
|
+
```tsx
|
|
850
|
+
import { loadDocument, preloadDocument, clearDocumentCache } from 'pdfjs-reader-core';
|
|
851
|
+
|
|
852
|
+
// Preload next document
|
|
853
|
+
await preloadDocument('/next-doc.pdf');
|
|
854
|
+
|
|
855
|
+
// Clear cache to free memory
|
|
856
|
+
clearDocumentCache('/doc.pdf');
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## Browser Support
|
|
862
|
+
|
|
863
|
+
- Chrome (recommended)
|
|
864
|
+
- Firefox
|
|
865
|
+
- Safari
|
|
866
|
+
- Edge
|
|
867
|
+
|
|
868
|
+
## License
|
|
869
|
+
|
|
870
|
+
MIT
|