@octanejs/pdf 0.0.1
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 +21 -0
- package/README.md +54 -0
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/UPSTREAM.md +30 -0
- package/dist/Page/AnnotationLayer.css +333 -0
- package/dist/Page/TextLayer.css +119 -0
- package/package.json +80 -0
- package/src/LinkService.ts +128 -0
- package/src/PasswordResponses.ts +6 -0
- package/src/Ref.ts +15 -0
- package/src/components.tsrx +887 -0
- package/src/contexts.ts +19 -0
- package/src/index.server.ts +18 -0
- package/src/index.ts +20 -0
- package/src/refs.ts +14 -0
- package/src/types.ts +205 -0
- package/src/utils.ts +27 -0
- package/src/vite-env.d.ts +4 -0
- package/upstream-public-surface.json +65 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import { createElement, isChildrenBlock, useEffect, useMemo, useRef, useState } from 'octane';
|
|
2
|
+
import makeEventProps from 'make-event-props';
|
|
3
|
+
import type {
|
|
4
|
+
PDFDocumentLoadingTask,
|
|
5
|
+
PDFDocumentProxy,
|
|
6
|
+
PDFPageProxy,
|
|
7
|
+
PageViewport,
|
|
8
|
+
} from 'pdfjs-dist';
|
|
9
|
+
|
|
10
|
+
import { DocumentContext, OutlineContext, PageContext, useDocumentContext } from './contexts.js';
|
|
11
|
+
import LinkService from './LinkService.js';
|
|
12
|
+
import PasswordResponses from './PasswordResponses.js';
|
|
13
|
+
import Ref from './Ref.js';
|
|
14
|
+
import { setRef } from './refs.js';
|
|
15
|
+
import { convertDataUriParameterObject, dataURItoBytes, isDataURI } from './utils.js';
|
|
16
|
+
import type {
|
|
17
|
+
Annotations,
|
|
18
|
+
DocumentContextValue,
|
|
19
|
+
DocumentProps,
|
|
20
|
+
DocumentRenderProps,
|
|
21
|
+
NodeOrRenderer,
|
|
22
|
+
OutlineProps,
|
|
23
|
+
PageCallback,
|
|
24
|
+
PageProps,
|
|
25
|
+
PageRenderProps,
|
|
26
|
+
Source,
|
|
27
|
+
StructTreeNode,
|
|
28
|
+
TextItem,
|
|
29
|
+
ThumbnailProps,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
|
|
32
|
+
const BLOCKED_CUSTOM_TEXT_TAGS = new Set([
|
|
33
|
+
'base',
|
|
34
|
+
'embed',
|
|
35
|
+
'iframe',
|
|
36
|
+
'link',
|
|
37
|
+
'meta',
|
|
38
|
+
'object',
|
|
39
|
+
'script',
|
|
40
|
+
'style',
|
|
41
|
+
'template',
|
|
42
|
+
]);
|
|
43
|
+
const URL_ATTRIBUTES = new Set(['action', 'formaction', 'href', 'poster', 'src', 'xlink:href']);
|
|
44
|
+
|
|
45
|
+
function isDangerousUrl(value: string): boolean {
|
|
46
|
+
return value.replace(/[\u0000-\u0020\u007f]/g, '').toLowerCase().match(
|
|
47
|
+
/^(?:javascript|vbscript):/,
|
|
48
|
+
) !==
|
|
49
|
+
null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sanitizeCustomTextNode(node: ChildNode): Node {
|
|
53
|
+
if (!(node instanceof HTMLElement)) return document.createTextNode(node.textContent ?? '');
|
|
54
|
+
if (BLOCKED_CUSTOM_TEXT_TAGS.has(node.tagName.toLowerCase())) {
|
|
55
|
+
return document.createTextNode(node.textContent ?? '');
|
|
56
|
+
}
|
|
57
|
+
const sanitized = document.createElement(node.tagName.toLowerCase());
|
|
58
|
+
for (const attribute of node.attributes) {
|
|
59
|
+
const name = attribute.name.toLowerCase();
|
|
60
|
+
if (name.startsWith('on') || name === 'srcdoc') continue;
|
|
61
|
+
if (URL_ATTRIBUTES.has(name) && isDangerousUrl(attribute.value)) continue;
|
|
62
|
+
sanitized.setAttribute(attribute.name, attribute.value);
|
|
63
|
+
}
|
|
64
|
+
for (const child of node.childNodes) sanitized.append(sanitizeCustomTextNode(child));
|
|
65
|
+
return sanitized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderSafeCustomText(target: Element, content: string): void {
|
|
69
|
+
const template = document.createElement('template');
|
|
70
|
+
template.innerHTML = content;
|
|
71
|
+
const fragment = document.createDocumentFragment();
|
|
72
|
+
for (const child of template.content.childNodes) fragment.append(sanitizeCustomTextNode(child));
|
|
73
|
+
target.replaceChildren(fragment);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderMessage(value: NodeOrRenderer): unknown {
|
|
77
|
+
return typeof value === 'function' ? value() : value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function withEventData(props: Record<string, unknown>, value: unknown): Record<string, unknown> {
|
|
81
|
+
return makeEventProps(props as never, () => value) as Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function resolveSource(file: DocumentProps['file']): Promise<Source | null> {
|
|
85
|
+
if (!file) return null;
|
|
86
|
+
if (typeof file === 'string') {
|
|
87
|
+
return isDataURI(file) ? { data: dataURItoBytes(file) } : { url: file };
|
|
88
|
+
}
|
|
89
|
+
if (file instanceof ArrayBuffer) return { data: file };
|
|
90
|
+
if (typeof Blob !== 'undefined' && file instanceof Blob) {
|
|
91
|
+
return { data: await file.arrayBuffer() };
|
|
92
|
+
}
|
|
93
|
+
if (typeof file === 'object' && ('data' in file || 'range' in file || 'url' in file)) {
|
|
94
|
+
if ('url' in file && typeof file.url === 'string' && isDataURI(file.url)) {
|
|
95
|
+
return convertDataUriParameterObject(file);
|
|
96
|
+
}
|
|
97
|
+
return file;
|
|
98
|
+
}
|
|
99
|
+
throw new Error('Invalid PDF source.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultOnPassword(callback: (password: string | null) => void, reason: number): void {
|
|
103
|
+
if (typeof prompt !== 'function') return callback(null);
|
|
104
|
+
callback(
|
|
105
|
+
prompt(
|
|
106
|
+
reason === PasswordResponses.INCORRECT_PASSWORD
|
|
107
|
+
? 'Invalid password. Please try again.'
|
|
108
|
+
: 'Enter the password to open this PDF file.',
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function Document(props: DocumentProps = {}) {
|
|
114
|
+
const {
|
|
115
|
+
children,
|
|
116
|
+
class: classProp,
|
|
117
|
+
className,
|
|
118
|
+
error = 'Failed to load PDF file.',
|
|
119
|
+
externalLinkRel,
|
|
120
|
+
externalLinkTarget,
|
|
121
|
+
file,
|
|
122
|
+
imageResourcesPath,
|
|
123
|
+
inputRef,
|
|
124
|
+
loading = 'Loading PDF…',
|
|
125
|
+
noData = 'No PDF file specified.',
|
|
126
|
+
onItemClick,
|
|
127
|
+
onLoadError,
|
|
128
|
+
onLoadProgress,
|
|
129
|
+
onLoadSuccess,
|
|
130
|
+
onPassword = defaultOnPassword,
|
|
131
|
+
onSourceError,
|
|
132
|
+
onSourceSuccess,
|
|
133
|
+
options,
|
|
134
|
+
ref,
|
|
135
|
+
renderMode,
|
|
136
|
+
rotate,
|
|
137
|
+
scale,
|
|
138
|
+
...hostProps
|
|
139
|
+
} = props;
|
|
140
|
+
const documentElement = useRef<HTMLDivElement | null>(null);
|
|
141
|
+
const linkServiceRef = useRef<LinkService | null>(null);
|
|
142
|
+
const pagesRef = useRef<HTMLDivElement[]>([]);
|
|
143
|
+
const [source, setSource] = useState<Source | false | null | undefined>(undefined);
|
|
144
|
+
const [sourceError, setSourceError] = useState<Error | null>(null);
|
|
145
|
+
const [pdf, setPdf] = useState<PDFDocumentProxy | false | undefined>(undefined);
|
|
146
|
+
const [pdfError, setPdfError] = useState<Error | null>(null);
|
|
147
|
+
|
|
148
|
+
if (!linkServiceRef.current) linkServiceRef.current = new LinkService();
|
|
149
|
+
const linkService = linkServiceRef.current;
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
setRef(inputRef, documentElement.current);
|
|
153
|
+
return () => setRef(inputRef, null);
|
|
154
|
+
}, [inputRef]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const imperative = {
|
|
158
|
+
linkService: linkServiceRef,
|
|
159
|
+
pages: pagesRef,
|
|
160
|
+
viewer: {
|
|
161
|
+
current: {
|
|
162
|
+
scrollPageIntoView({ pageIndex = 0 }: { pageIndex?: number }) {
|
|
163
|
+
pagesRef.current[pageIndex]?.scrollIntoView();
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
setRef(ref as never, imperative);
|
|
169
|
+
return () => setRef(ref as never, null);
|
|
170
|
+
}, [ref]);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
let active = true;
|
|
174
|
+
setSource(undefined);
|
|
175
|
+
setSourceError(null);
|
|
176
|
+
void resolveSource(file).then((nextSource) => {
|
|
177
|
+
if (!active) return;
|
|
178
|
+
setSource(nextSource);
|
|
179
|
+
onSourceSuccess?.();
|
|
180
|
+
}).catch((caught: unknown) => {
|
|
181
|
+
if (!active) return;
|
|
182
|
+
const nextError =
|
|
183
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
184
|
+
setSource(false);
|
|
185
|
+
setSourceError(nextError);
|
|
186
|
+
onSourceError?.(nextError);
|
|
187
|
+
});
|
|
188
|
+
return () => {
|
|
189
|
+
active = false;
|
|
190
|
+
};
|
|
191
|
+
}, [file]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let loadingTask: PDFDocumentLoadingTask | undefined;
|
|
195
|
+
let active = true;
|
|
196
|
+
setPdf(undefined);
|
|
197
|
+
setPdfError(null);
|
|
198
|
+
if (!source) return;
|
|
199
|
+
|
|
200
|
+
void import('pdfjs-dist').then((pdfjs) => {
|
|
201
|
+
if (!active) return;
|
|
202
|
+
loadingTask = pdfjs.getDocument({ ...source, ...options } as never);
|
|
203
|
+
if (onLoadProgress) loadingTask.onProgress = onLoadProgress;
|
|
204
|
+
loadingTask.onPassword = onPassword;
|
|
205
|
+
return loadingTask.promise;
|
|
206
|
+
}).then((nextPdf) => {
|
|
207
|
+
if (!active || !nextPdf) return;
|
|
208
|
+
setPdf(nextPdf);
|
|
209
|
+
pagesRef.current = new Array(nextPdf.numPages);
|
|
210
|
+
linkService.setDocument(nextPdf);
|
|
211
|
+
onLoadSuccess?.(nextPdf);
|
|
212
|
+
}).catch((caught: unknown) => {
|
|
213
|
+
if (!active) return;
|
|
214
|
+
const nextError =
|
|
215
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
216
|
+
setPdf(false);
|
|
217
|
+
setPdfError(nextError);
|
|
218
|
+
onLoadError?.(nextError);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return () => {
|
|
222
|
+
active = false;
|
|
223
|
+
void loadingTask?.destroy();
|
|
224
|
+
};
|
|
225
|
+
}, [source, options]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
linkService.setExternalLinkRel(externalLinkRel);
|
|
229
|
+
linkService.setExternalLinkTarget(externalLinkTarget);
|
|
230
|
+
linkService.setViewer({
|
|
231
|
+
scrollPageIntoView({ pageIndex = 0 }) {
|
|
232
|
+
if (onItemClick) {
|
|
233
|
+
onItemClick({ pageIndex, pageNumber: pageIndex + 1 });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
pagesRef.current[pageIndex]?.scrollIntoView();
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}, [externalLinkRel, externalLinkTarget, onItemClick]);
|
|
240
|
+
|
|
241
|
+
const contextValue = useMemo<DocumentContextValue>(
|
|
242
|
+
() => ({
|
|
243
|
+
imageResourcesPath,
|
|
244
|
+
linkService,
|
|
245
|
+
onItemClick,
|
|
246
|
+
pdf,
|
|
247
|
+
registerPage(index, element) {
|
|
248
|
+
pagesRef.current[index] = element;
|
|
249
|
+
},
|
|
250
|
+
renderMode,
|
|
251
|
+
rotate,
|
|
252
|
+
scale,
|
|
253
|
+
unregisterPage(index) {
|
|
254
|
+
delete pagesRef.current[index];
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
[imageResourcesPath, linkService, onItemClick, pdf, renderMode, rotate, scale],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
let content: unknown;
|
|
261
|
+
if (!file) {
|
|
262
|
+
content = <div class="react-pdf__message react-pdf__message--no-data">{renderMessage(
|
|
263
|
+
noData,
|
|
264
|
+
)}</div>;
|
|
265
|
+
} else if (sourceError || pdfError || source === false || pdf === false) {
|
|
266
|
+
content = <div class="react-pdf__message react-pdf__message--error">{renderMessage(
|
|
267
|
+
error,
|
|
268
|
+
)}</div>;
|
|
269
|
+
} else if (!pdf) {
|
|
270
|
+
content = <div class="react-pdf__message react-pdf__message--loading">{renderMessage(
|
|
271
|
+
loading,
|
|
272
|
+
)}</div>;
|
|
273
|
+
} else {
|
|
274
|
+
content = typeof children === 'function' && !isChildrenBlock(children)
|
|
275
|
+
? children(contextValue as DocumentRenderProps)
|
|
276
|
+
: children;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return <div
|
|
280
|
+
{...withEventData(hostProps, pdf)}
|
|
281
|
+
ref={documentElement}
|
|
282
|
+
class={['react-pdf__Document', classProp, className]}
|
|
283
|
+
>
|
|
284
|
+
<DocumentContext.Provider value={contextValue}>{content}</DocumentContext.Provider>
|
|
285
|
+
</div>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function makePageCallback(page: PDFPageProxy, viewport: PageViewport): PageCallback {
|
|
289
|
+
const original = page.getViewport({ scale: 1, rotation: viewport.rotation });
|
|
290
|
+
return Object.assign(page, {
|
|
291
|
+
width: viewport.width,
|
|
292
|
+
height: viewport.height,
|
|
293
|
+
originalWidth: original.width,
|
|
294
|
+
originalHeight: original.height,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const PDF_ROLE_TO_HTML_ROLE: Record<string, string | undefined> = {
|
|
299
|
+
Part: 'group',
|
|
300
|
+
Sect: 'group',
|
|
301
|
+
Div: 'group',
|
|
302
|
+
Aside: 'note',
|
|
303
|
+
NonStruct: 'none',
|
|
304
|
+
H: 'heading',
|
|
305
|
+
FENote: 'note',
|
|
306
|
+
Sub: 'group',
|
|
307
|
+
Link: 'link',
|
|
308
|
+
Annot: 'note',
|
|
309
|
+
Form: 'form',
|
|
310
|
+
L: 'list',
|
|
311
|
+
LI: 'listitem',
|
|
312
|
+
Table: 'table',
|
|
313
|
+
TR: 'row',
|
|
314
|
+
TH: 'columnheader',
|
|
315
|
+
TD: 'cell',
|
|
316
|
+
THead: 'columnheader',
|
|
317
|
+
Figure: 'figure',
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
function StructTreeItem({ node, className }: { node: unknown; className?: string }) {
|
|
321
|
+
if (!node || typeof node !== 'object') return null;
|
|
322
|
+
const value = node as {
|
|
323
|
+
alt?: string;
|
|
324
|
+
children?: unknown[];
|
|
325
|
+
id?: string;
|
|
326
|
+
lang?: string;
|
|
327
|
+
role?: string;
|
|
328
|
+
};
|
|
329
|
+
const attributes: Record<string, unknown> = {};
|
|
330
|
+
if (value.alt !== undefined) attributes['aria-label'] = value.alt;
|
|
331
|
+
if (value.lang !== undefined) attributes.lang = value.lang;
|
|
332
|
+
if (value.id !== undefined) attributes['aria-owns'] = value.id;
|
|
333
|
+
if (value.role) {
|
|
334
|
+
const heading = /^H(\d+)$/.exec(value.role);
|
|
335
|
+
if (heading) {
|
|
336
|
+
attributes.role = 'heading';
|
|
337
|
+
attributes['aria-level'] = Number(heading[1]);
|
|
338
|
+
} else if (PDF_ROLE_TO_HTML_ROLE[value.role]) {
|
|
339
|
+
attributes.role = PDF_ROLE_TO_HTML_ROLE[value.role];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const onlyContentChild =
|
|
343
|
+
value.children?.length === 1 && value.children[0] != null &&
|
|
344
|
+
typeof value.children[0] === 'object' &&
|
|
345
|
+
'id' in value.children[0];
|
|
346
|
+
if (onlyContentChild) {
|
|
347
|
+
const child = value.children?.[0] as { id?: string };
|
|
348
|
+
if (child.id !== undefined) attributes['aria-owns'] = child.id;
|
|
349
|
+
}
|
|
350
|
+
return <span class={className} {...attributes}>
|
|
351
|
+
{onlyContentChild ? null : value.children?.map((child) => <StructTreeItem node={child} />)}
|
|
352
|
+
</span>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function Page(props: PageProps = {}) {
|
|
356
|
+
const documentContext = useDocumentContext();
|
|
357
|
+
const merged = { ...documentContext, ...props } as PageProps & Partial<DocumentContextValue>;
|
|
358
|
+
const {
|
|
359
|
+
_className = 'react-pdf__Page',
|
|
360
|
+
_enableRegisterUnregisterPage = true,
|
|
361
|
+
canvasBackground,
|
|
362
|
+
canvasRef,
|
|
363
|
+
children,
|
|
364
|
+
class: classProp,
|
|
365
|
+
className,
|
|
366
|
+
customRenderer,
|
|
367
|
+
customTextRenderer,
|
|
368
|
+
devicePixelRatio,
|
|
369
|
+
error = 'Failed to load the page.',
|
|
370
|
+
filterAnnotations,
|
|
371
|
+
height,
|
|
372
|
+
imageResourcesPath,
|
|
373
|
+
inputRef,
|
|
374
|
+
loading = 'Loading page…',
|
|
375
|
+
linkService: _linkService,
|
|
376
|
+
noData = 'No page specified.',
|
|
377
|
+
onGetAnnotationsError,
|
|
378
|
+
onGetAnnotationsSuccess,
|
|
379
|
+
onGetStructTreeError,
|
|
380
|
+
onGetStructTreeSuccess,
|
|
381
|
+
onGetTextError,
|
|
382
|
+
onGetTextSuccess,
|
|
383
|
+
onLoadError,
|
|
384
|
+
onLoadSuccess,
|
|
385
|
+
onItemClick: _onItemClick,
|
|
386
|
+
onRenderAnnotationLayerError,
|
|
387
|
+
onRenderAnnotationLayerSuccess,
|
|
388
|
+
onRenderError,
|
|
389
|
+
onRenderSuccess,
|
|
390
|
+
onRenderTextLayerError,
|
|
391
|
+
onRenderTextLayerSuccess,
|
|
392
|
+
pageColors,
|
|
393
|
+
pageIndex: pageIndexProp,
|
|
394
|
+
pageNumber: pageNumberProp,
|
|
395
|
+
pdf,
|
|
396
|
+
registerPage,
|
|
397
|
+
renderAnnotationLayer = true,
|
|
398
|
+
renderForms = false,
|
|
399
|
+
renderMode = 'canvas',
|
|
400
|
+
renderTextLayer = true,
|
|
401
|
+
rotate: rotateProp,
|
|
402
|
+
scale: scaleProp = 1,
|
|
403
|
+
unregisterPage,
|
|
404
|
+
width,
|
|
405
|
+
...hostProps
|
|
406
|
+
} = merged;
|
|
407
|
+
const pageElement = useRef<HTMLDivElement | null>(null);
|
|
408
|
+
const canvasElement = useRef<HTMLCanvasElement | null>(null);
|
|
409
|
+
const textElement = useRef<HTMLDivElement | null>(null);
|
|
410
|
+
const annotationElement = useRef<HTMLDivElement | null>(null);
|
|
411
|
+
const [page, setPage] = useState<PDFPageProxy | false | undefined>(undefined);
|
|
412
|
+
const [pageError, setPageError] = useState<Error | null>(null);
|
|
413
|
+
const [structTree, setStructTree] = useState<StructTreeNode | null | undefined>(undefined);
|
|
414
|
+
const pageIndex =
|
|
415
|
+
pageNumberProp != null ? pageNumberProp - 1 : pageIndexProp;
|
|
416
|
+
const pageNumber = pageNumberProp ?? (pageIndexProp != null ? pageIndexProp + 1 : undefined);
|
|
417
|
+
|
|
418
|
+
if (pdf === undefined && documentContext === null) {
|
|
419
|
+
throw new Error('Attempted to load a page, but no document was specified.');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const viewport = useMemo(() => {
|
|
423
|
+
if (!page || !pageNumber) return null;
|
|
424
|
+
const rotation = rotateProp ?? page.rotate;
|
|
425
|
+
const base = page.getViewport({ scale: 1, rotation });
|
|
426
|
+
const sizeScale =
|
|
427
|
+
width
|
|
428
|
+
? width / base.width
|
|
429
|
+
: height
|
|
430
|
+
? height / base.height
|
|
431
|
+
: 1;
|
|
432
|
+
return page.getViewport({ scale: scaleProp * sizeScale, rotation });
|
|
433
|
+
}, [height, page, pageNumber, rotateProp, scaleProp, width]);
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
let active = true;
|
|
437
|
+
setPage(undefined);
|
|
438
|
+
setPageError(null);
|
|
439
|
+
if (!pdf || !pageNumber) return;
|
|
440
|
+
void pdf.getPage(pageNumber).then((nextPage) => {
|
|
441
|
+
if (!active) return;
|
|
442
|
+
setPage(nextPage);
|
|
443
|
+
}).catch((caught: unknown) => {
|
|
444
|
+
if (!active) return;
|
|
445
|
+
const nextError =
|
|
446
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
447
|
+
setPage(false);
|
|
448
|
+
setPageError(nextError);
|
|
449
|
+
onLoadError?.(nextError);
|
|
450
|
+
});
|
|
451
|
+
return () => {
|
|
452
|
+
active = false;
|
|
453
|
+
};
|
|
454
|
+
}, [pdf, pageNumber]);
|
|
455
|
+
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (!page || !viewport || pageIndex == null) return;
|
|
458
|
+
const callback = makePageCallback(page, viewport);
|
|
459
|
+
onLoadSuccess?.(callback);
|
|
460
|
+
if (_enableRegisterUnregisterPage && pageElement.current) {
|
|
461
|
+
registerPage?.(pageIndex, pageElement.current);
|
|
462
|
+
}
|
|
463
|
+
return () => {
|
|
464
|
+
if (_enableRegisterUnregisterPage) unregisterPage?.(pageIndex);
|
|
465
|
+
};
|
|
466
|
+
}, [page, viewport, pageIndex]);
|
|
467
|
+
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
setRef(inputRef, pageElement.current);
|
|
470
|
+
return () => setRef(inputRef, null);
|
|
471
|
+
}, [inputRef]);
|
|
472
|
+
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
setRef(canvasRef, canvasElement.current);
|
|
475
|
+
return () => setRef(canvasRef, null);
|
|
476
|
+
}, [canvasRef, page, renderMode]);
|
|
477
|
+
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (!page || !viewport || renderMode !== 'canvas' || !canvasElement.current) return;
|
|
480
|
+
let active = true;
|
|
481
|
+
let renderTask: { cancel: () => void; promise: Promise<unknown> } | undefined;
|
|
482
|
+
const canvas = canvasElement.current;
|
|
483
|
+
const ratio = devicePixelRatio ?? window.devicePixelRatio ?? 1;
|
|
484
|
+
const renderViewport = page.getViewport({
|
|
485
|
+
rotation: viewport.rotation,
|
|
486
|
+
scale: viewport.scale * ratio,
|
|
487
|
+
});
|
|
488
|
+
page.cleanup();
|
|
489
|
+
canvas.width = Math.floor(renderViewport.width);
|
|
490
|
+
canvas.height = Math.floor(renderViewport.height);
|
|
491
|
+
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
|
492
|
+
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
|
493
|
+
canvas.style.visibility = 'hidden';
|
|
494
|
+
void import('pdfjs-dist').then((pdfjs) => {
|
|
495
|
+
if (!active) return;
|
|
496
|
+
renderTask = page.render({
|
|
497
|
+
annotationMode: renderForms
|
|
498
|
+
? pdfjs.AnnotationMode.ENABLE_FORMS
|
|
499
|
+
: pdfjs.AnnotationMode.ENABLE,
|
|
500
|
+
canvas,
|
|
501
|
+
canvasContext: canvas.getContext('2d', { alpha: false })!,
|
|
502
|
+
background: canvasBackground,
|
|
503
|
+
pageColors,
|
|
504
|
+
viewport: renderViewport,
|
|
505
|
+
});
|
|
506
|
+
return renderTask.promise;
|
|
507
|
+
}).then(() => {
|
|
508
|
+
if (!active || !renderTask) return;
|
|
509
|
+
canvas.style.visibility = '';
|
|
510
|
+
onRenderSuccess?.(makePageCallback(page, viewport));
|
|
511
|
+
}).catch((caught: unknown) => {
|
|
512
|
+
if (!active || caught instanceof Error && caught.name === 'AbortException') return;
|
|
513
|
+
onRenderError?.(caught instanceof Error ? caught : new Error(String(caught)));
|
|
514
|
+
});
|
|
515
|
+
return () => {
|
|
516
|
+
active = false;
|
|
517
|
+
renderTask?.cancel();
|
|
518
|
+
canvas.width = 0;
|
|
519
|
+
canvas.height = 0;
|
|
520
|
+
};
|
|
521
|
+
}, [canvasBackground, devicePixelRatio, page, pageColors, renderForms, renderMode, viewport]);
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
if (!page || !viewport || pageIndex == null || pageNumber == null || !renderTextLayer) return;
|
|
525
|
+
let active = true;
|
|
526
|
+
let textLayer: { cancel: () => void; render: () => Promise<void> } | undefined;
|
|
527
|
+
let contentLoaded = false;
|
|
528
|
+
const element = textElement.current;
|
|
529
|
+
if (!element) return;
|
|
530
|
+
element.replaceChildren();
|
|
531
|
+
void page.getTextContent().then(async (content) => {
|
|
532
|
+
if (!active) return;
|
|
533
|
+
contentLoaded = true;
|
|
534
|
+
onGetTextSuccess?.(content);
|
|
535
|
+
const pdfjs = await import('pdfjs-dist');
|
|
536
|
+
if (!active) return;
|
|
537
|
+
textLayer = new pdfjs.TextLayer({
|
|
538
|
+
container: element,
|
|
539
|
+
textContentSource: page.streamTextContent({ includeMarkedContent: true }),
|
|
540
|
+
viewport,
|
|
541
|
+
}) as { cancel: () => void; render: () => Promise<void> };
|
|
542
|
+
await textLayer.render();
|
|
543
|
+
if (!active) return;
|
|
544
|
+
const end = document.createElement('div');
|
|
545
|
+
end.className = 'endOfContent';
|
|
546
|
+
element.append(end);
|
|
547
|
+
if (customTextRenderer) {
|
|
548
|
+
const children = element.querySelectorAll('[role="presentation"]');
|
|
549
|
+
let childIndex = 0;
|
|
550
|
+
content.items.forEach((item, itemIndex) => {
|
|
551
|
+
if (!('str' in item)) return;
|
|
552
|
+
const child = children[childIndex];
|
|
553
|
+
if (child) {
|
|
554
|
+
renderSafeCustomText(
|
|
555
|
+
child,
|
|
556
|
+
customTextRenderer({
|
|
557
|
+
...item,
|
|
558
|
+
itemIndex,
|
|
559
|
+
pageIndex,
|
|
560
|
+
pageNumber,
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
childIndex += item.str && item.hasEOL ? 2 : 1;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
onRenderTextLayerSuccess?.();
|
|
568
|
+
}).catch((caught: unknown) => {
|
|
569
|
+
if (!active || caught instanceof Error && caught.name === 'AbortException') return;
|
|
570
|
+
const error =
|
|
571
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
572
|
+
if (contentLoaded) onRenderTextLayerError?.(error);
|
|
573
|
+
else onGetTextError?.(error);
|
|
574
|
+
});
|
|
575
|
+
return () => {
|
|
576
|
+
active = false;
|
|
577
|
+
textLayer?.cancel();
|
|
578
|
+
element.replaceChildren();
|
|
579
|
+
};
|
|
580
|
+
}, [customTextRenderer, page, pageIndex, pageNumber, renderTextLayer, viewport]);
|
|
581
|
+
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (!page || !viewport || !pdf || !renderAnnotationLayer) return;
|
|
584
|
+
let active = true;
|
|
585
|
+
let annotationsLoaded = false;
|
|
586
|
+
const element = annotationElement.current;
|
|
587
|
+
if (!element) return;
|
|
588
|
+
element.replaceChildren();
|
|
589
|
+
void page.getAnnotations().then(async (loadedAnnotations) => {
|
|
590
|
+
if (!active) return;
|
|
591
|
+
annotationsLoaded = true;
|
|
592
|
+
onGetAnnotationsSuccess?.(loadedAnnotations);
|
|
593
|
+
const annotations =
|
|
594
|
+
filterAnnotations
|
|
595
|
+
? filterAnnotations({ annotations: loadedAnnotations })
|
|
596
|
+
: loadedAnnotations;
|
|
597
|
+
const pdfjs = await import('pdfjs-dist');
|
|
598
|
+
if (!active) return;
|
|
599
|
+
const annotationViewport = viewport.clone({ dontFlip: true });
|
|
600
|
+
const parameters = {
|
|
601
|
+
accessibilityManager: null,
|
|
602
|
+
annotationCanvasMap: null,
|
|
603
|
+
annotationEditorUIManager: null,
|
|
604
|
+
annotationStorage: pdf.annotationStorage,
|
|
605
|
+
commentManager: null,
|
|
606
|
+
div: element,
|
|
607
|
+
l10n: null,
|
|
608
|
+
linkService: documentContext?.linkService,
|
|
609
|
+
page,
|
|
610
|
+
structTreeLayer: null,
|
|
611
|
+
viewport: annotationViewport,
|
|
612
|
+
};
|
|
613
|
+
new pdfjs.AnnotationLayer(parameters as never).render(
|
|
614
|
+
{
|
|
615
|
+
annotations,
|
|
616
|
+
annotationStorage: pdf.annotationStorage,
|
|
617
|
+
div: element,
|
|
618
|
+
imageResourcesPath,
|
|
619
|
+
linkService: documentContext?.linkService,
|
|
620
|
+
page,
|
|
621
|
+
renderForms,
|
|
622
|
+
viewport: annotationViewport,
|
|
623
|
+
} as never,
|
|
624
|
+
);
|
|
625
|
+
onRenderAnnotationLayerSuccess?.();
|
|
626
|
+
}).catch((caught: unknown) => {
|
|
627
|
+
if (!active) return;
|
|
628
|
+
const error =
|
|
629
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
630
|
+
if (annotationsLoaded) onRenderAnnotationLayerError?.(error);
|
|
631
|
+
else onGetAnnotationsError?.(error);
|
|
632
|
+
});
|
|
633
|
+
return () => {
|
|
634
|
+
active = false;
|
|
635
|
+
element.replaceChildren();
|
|
636
|
+
};
|
|
637
|
+
}, [filterAnnotations, imageResourcesPath, page, pdf, renderAnnotationLayer, renderForms, viewport]);
|
|
638
|
+
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
let active = true;
|
|
641
|
+
setStructTree(undefined);
|
|
642
|
+
if (!page || customTextRenderer || !renderTextLayer) return;
|
|
643
|
+
void page.getStructTree().then((tree) => {
|
|
644
|
+
if (!active) return;
|
|
645
|
+
setStructTree(tree);
|
|
646
|
+
if (tree) onGetStructTreeSuccess?.(tree);
|
|
647
|
+
}).catch((caught: unknown) => {
|
|
648
|
+
if (!active) return;
|
|
649
|
+
const error =
|
|
650
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
651
|
+
setStructTree(null);
|
|
652
|
+
onGetStructTreeError?.(error);
|
|
653
|
+
});
|
|
654
|
+
return () => {
|
|
655
|
+
active = false;
|
|
656
|
+
};
|
|
657
|
+
}, [customTextRenderer, page, renderTextLayer]);
|
|
658
|
+
|
|
659
|
+
const contextValue =
|
|
660
|
+
page && viewport && pageIndex != null && pageNumber != null
|
|
661
|
+
? {
|
|
662
|
+
page,
|
|
663
|
+
pageIndex,
|
|
664
|
+
pageNumber,
|
|
665
|
+
rotate: viewport.rotation,
|
|
666
|
+
scale: viewport.scale,
|
|
667
|
+
}
|
|
668
|
+
: null;
|
|
669
|
+
|
|
670
|
+
let content: unknown;
|
|
671
|
+
if (!pageNumber) {
|
|
672
|
+
content = <div class="react-pdf__message react-pdf__message--no-data">{renderMessage(
|
|
673
|
+
noData,
|
|
674
|
+
)}</div>;
|
|
675
|
+
} else if (pdf === false || page === false || pageError) {
|
|
676
|
+
content = <div class="react-pdf__message react-pdf__message--error">{renderMessage(
|
|
677
|
+
error,
|
|
678
|
+
)}</div>;
|
|
679
|
+
} else if (!page || !viewport || !contextValue) {
|
|
680
|
+
content = <div class="react-pdf__message react-pdf__message--loading">{renderMessage(
|
|
681
|
+
loading,
|
|
682
|
+
)}</div>;
|
|
683
|
+
} else {
|
|
684
|
+
if (renderMode === 'custom' && !customRenderer) {
|
|
685
|
+
throw new Error('renderMode was set to "custom", but no customRenderer was passed.');
|
|
686
|
+
}
|
|
687
|
+
const renderProps = contextValue as PageRenderProps;
|
|
688
|
+
content = <PageContext.Provider value={contextValue}>
|
|
689
|
+
{renderMode === 'canvas'
|
|
690
|
+
? <canvas
|
|
691
|
+
ref={canvasElement}
|
|
692
|
+
class="react-pdf__Page__canvas"
|
|
693
|
+
dir="ltr"
|
|
694
|
+
style={{ display: 'block', userSelect: 'none' }}
|
|
695
|
+
>
|
|
696
|
+
{renderTextLayer && structTree
|
|
697
|
+
? <StructTreeItem
|
|
698
|
+
className="react-pdf__Page__structTree structTree"
|
|
699
|
+
node={structTree}
|
|
700
|
+
/>
|
|
701
|
+
: null}
|
|
702
|
+
</canvas>
|
|
703
|
+
: null}
|
|
704
|
+
{renderMode === 'custom' && customRenderer ? createElement(customRenderer, {}) : null}
|
|
705
|
+
{renderTextLayer
|
|
706
|
+
? <div ref={textElement} class="react-pdf__Page__textContent textLayer" />
|
|
707
|
+
: null}
|
|
708
|
+
{renderAnnotationLayer
|
|
709
|
+
? <div ref={annotationElement} class="react-pdf__Page__annotations annotationLayer" />
|
|
710
|
+
: null}
|
|
711
|
+
{typeof children === 'function' && !isChildrenBlock(children)
|
|
712
|
+
? children(renderProps)
|
|
713
|
+
: children}
|
|
714
|
+
</PageContext.Provider>;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return <div
|
|
718
|
+
{...withEventData(hostProps, page && viewport ? makePageCallback(page, viewport) : page)}
|
|
719
|
+
ref={pageElement}
|
|
720
|
+
class={[_className, classProp, className]}
|
|
721
|
+
data-page-number={pageNumber}
|
|
722
|
+
style={{
|
|
723
|
+
'--scale-round-x': '1px',
|
|
724
|
+
'--scale-round-y': '1px',
|
|
725
|
+
'--scale-factor': '1',
|
|
726
|
+
'--user-unit': `${viewport?.scale ?? scaleProp}`,
|
|
727
|
+
'--total-scale-factor': 'calc(var(--scale-factor) * var(--user-unit))',
|
|
728
|
+
backgroundColor: canvasBackground || 'white',
|
|
729
|
+
minHeight: 'min-content',
|
|
730
|
+
minWidth: 'min-content',
|
|
731
|
+
position: 'relative',
|
|
732
|
+
} as never}
|
|
733
|
+
>{content}</div>;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function Thumbnail(props: ThumbnailProps = {}) {
|
|
737
|
+
const documentContext = useDocumentContext();
|
|
738
|
+
const merged = { ...documentContext, ...props } as ThumbnailProps & DocumentContextValue;
|
|
739
|
+
const { class: classProp, className, onItemClick, pageIndex, pageNumber, pdf } = merged;
|
|
740
|
+
if (!pdf) {
|
|
741
|
+
throw new Error('Attempted to load a thumbnail, but no document was specified.');
|
|
742
|
+
}
|
|
743
|
+
const resolvedPageNumber = pageNumber ?? (pageIndex != null ? pageIndex + 1 : undefined);
|
|
744
|
+
const resolvedPageIndex =
|
|
745
|
+
resolvedPageNumber != null ? resolvedPageNumber - 1 : 0;
|
|
746
|
+
|
|
747
|
+
function handleClick(event: MouseEvent) {
|
|
748
|
+
event.preventDefault();
|
|
749
|
+
if (resolvedPageNumber == null) return;
|
|
750
|
+
const args = { pageIndex: resolvedPageIndex, pageNumber: resolvedPageNumber };
|
|
751
|
+
if (onItemClick) onItemClick(args);
|
|
752
|
+
else if (documentContext?.linkService) documentContext.linkService.goToPage(resolvedPageNumber);
|
|
753
|
+
else throw new Error('Either onItemClick callback or linkService must be defined in order to navigate to an outline item.');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const { class: _class, className: _className, onItemClick: _onItemClick, ...pageProps } = props;
|
|
757
|
+
return <a
|
|
758
|
+
class={['react-pdf__Thumbnail', classProp, className]}
|
|
759
|
+
href={resolvedPageNumber ? '#' : undefined}
|
|
760
|
+
onClick={handleClick}
|
|
761
|
+
>
|
|
762
|
+
<Page
|
|
763
|
+
{...pageProps}
|
|
764
|
+
_className="react-pdf__Thumbnail__page"
|
|
765
|
+
_enableRegisterUnregisterPage={false}
|
|
766
|
+
pageIndex={pageIndex}
|
|
767
|
+
pageNumber={pageNumber}
|
|
768
|
+
renderAnnotationLayer={false}
|
|
769
|
+
renderTextLayer={false}
|
|
770
|
+
/>
|
|
771
|
+
</a>;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
type OutlineEntry = NonNullable<Awaited<ReturnType<PDFDocumentProxy['getOutline']>>>[number];
|
|
775
|
+
|
|
776
|
+
function OutlineItem({
|
|
777
|
+
item,
|
|
778
|
+
linkService,
|
|
779
|
+
pdf,
|
|
780
|
+
onItemClick,
|
|
781
|
+
}: {
|
|
782
|
+
item: OutlineEntry;
|
|
783
|
+
linkService?: LinkService;
|
|
784
|
+
pdf: PDFDocumentProxy;
|
|
785
|
+
onItemClick?: OutlineProps['onItemClick'];
|
|
786
|
+
}) {
|
|
787
|
+
async function activate(event: Event) {
|
|
788
|
+
event.preventDefault();
|
|
789
|
+
if (!onItemClick && !linkService) {
|
|
790
|
+
throw new Error('Either onItemClick callback or linkService must be defined in order to navigate to an outline item.');
|
|
791
|
+
}
|
|
792
|
+
if (!onItemClick) {
|
|
793
|
+
void linkService?.goToDestination(item.dest);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const dest =
|
|
797
|
+
typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
|
|
798
|
+
if (!dest) throw new Error('Destination not found.');
|
|
799
|
+
const reference = dest[0];
|
|
800
|
+
const pageIndex =
|
|
801
|
+
typeof reference === 'number' ? reference : await pdf.getPageIndex(new Ref(reference));
|
|
802
|
+
const args = { dest, pageIndex, pageNumber: pageIndex + 1 };
|
|
803
|
+
onItemClick(args);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return <li>
|
|
807
|
+
<a href="#" onClick={activate}>{item.title}</a>
|
|
808
|
+
{item.items?.length
|
|
809
|
+
? <ul>{item.items.map(
|
|
810
|
+
(child) =>
|
|
811
|
+
<OutlineItem
|
|
812
|
+
item={child}
|
|
813
|
+
linkService={linkService}
|
|
814
|
+
pdf={pdf}
|
|
815
|
+
onItemClick={onItemClick}
|
|
816
|
+
/>,
|
|
817
|
+
)}</ul>
|
|
818
|
+
: null}
|
|
819
|
+
</li>;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export function Outline(props: OutlineProps = {}) {
|
|
823
|
+
const documentContext = useDocumentContext();
|
|
824
|
+
const {
|
|
825
|
+
class: classProp,
|
|
826
|
+
className,
|
|
827
|
+
inputRef,
|
|
828
|
+
onItemClick,
|
|
829
|
+
onLoadError,
|
|
830
|
+
onLoadSuccess,
|
|
831
|
+
pdf = documentContext?.pdf,
|
|
832
|
+
...hostProps
|
|
833
|
+
} = props;
|
|
834
|
+
const outlineElement = useRef<HTMLDivElement | null>(null);
|
|
835
|
+
const [outline, setOutline] = useState<
|
|
836
|
+
Awaited<ReturnType<PDFDocumentProxy['getOutline']>> | undefined
|
|
837
|
+
>();
|
|
838
|
+
const [failed, setFailed] = useState(false);
|
|
839
|
+
if (!pdf) {
|
|
840
|
+
throw new Error('Attempted to load an outline, but no document was specified.');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
setRef(inputRef, outlineElement.current);
|
|
845
|
+
return () => setRef(inputRef, null);
|
|
846
|
+
}, [inputRef]);
|
|
847
|
+
|
|
848
|
+
useEffect(() => {
|
|
849
|
+
let active = true;
|
|
850
|
+
setOutline(undefined);
|
|
851
|
+
setFailed(false);
|
|
852
|
+
void pdf.getOutline().then((nextOutline) => {
|
|
853
|
+
if (!active) return;
|
|
854
|
+
setOutline(nextOutline);
|
|
855
|
+
onLoadSuccess?.(nextOutline);
|
|
856
|
+
}).catch((caught: unknown) => {
|
|
857
|
+
if (!active) return;
|
|
858
|
+
const error =
|
|
859
|
+
caught instanceof Error ? caught : new Error(String(caught));
|
|
860
|
+
setFailed(true);
|
|
861
|
+
onLoadError?.(error);
|
|
862
|
+
});
|
|
863
|
+
return () => {
|
|
864
|
+
active = false;
|
|
865
|
+
};
|
|
866
|
+
}, [pdf]);
|
|
867
|
+
|
|
868
|
+
if (failed || !outline) return null;
|
|
869
|
+
const clickHandler = onItemClick ?? documentContext?.onItemClick;
|
|
870
|
+
return <div
|
|
871
|
+
{...withEventData(hostProps, outline)}
|
|
872
|
+
ref={outlineElement}
|
|
873
|
+
class={['react-pdf__Outline', classProp, className]}
|
|
874
|
+
>
|
|
875
|
+
<OutlineContext.Provider value={{ onItemClick: clickHandler }}>
|
|
876
|
+
<ul>{outline.map(
|
|
877
|
+
(item) =>
|
|
878
|
+
<OutlineItem
|
|
879
|
+
item={item}
|
|
880
|
+
linkService={documentContext?.linkService}
|
|
881
|
+
pdf={pdf}
|
|
882
|
+
onItemClick={clickHandler}
|
|
883
|
+
/>,
|
|
884
|
+
)}</ul>
|
|
885
|
+
</OutlineContext.Provider>
|
|
886
|
+
</div>;
|
|
887
|
+
}
|