@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.
@@ -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
+ }