vue-pdfjs-headless 4.3.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,12 +5,14 @@
5
5
  [![NPM downloads][badge-npm-downloads]][url-npm]
6
6
  [![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
7
7
 
8
- Headless Vue composables over pdfjs-dist: a document's loading, a page's drawing and the viewer's state, with no styling and no renderer of its own.
8
+ Headless Vue composables over pdfjs-dist that load a document, draw its pages and track the viewer's state, with no styling and no renderer of its own.
9
+
10
+ Design notes live in [the specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/vue-pdfjs-headless).
9
11
 
10
12
  ## Table of Contents
11
13
 
12
14
  - 🚀 [Getting Started](#getting-started)
13
- - 📖 [Documentation](#documentation)
15
+ - 🧩 [Key Exports](#key-exports)
14
16
  - ⚖️ [License](#license)
15
17
 
16
18
  ---
@@ -21,9 +23,42 @@ Headless Vue composables over pdfjs-dist: a document's loading, a page's drawing
21
23
  pnpm i vue-pdfjs-headless
22
24
  ```
23
25
 
24
- ## <a name="documentation">📖 Documentation</a>
26
+ ```ts
27
+ import * as pdfjs from "pdfjs-dist";
28
+ import { shallowRef } from "vue";
29
+ import { usePdfDocument, usePdfPage } from "vue-pdfjs-headless";
30
+
31
+ const canvas = shallowRef<HTMLCanvasElement>();
32
+ const { pdfDocument } = usePdfDocument({
33
+ renderer: {
34
+ api: pdfjs,
35
+ cMapUrl: "/cmaps/",
36
+ standardFontDataUrl: "/fonts/",
37
+ wasmUrl: "/wasm/",
38
+ workerSrc: "/pdf.worker.mjs",
39
+ },
40
+ source: "/document.pdf",
41
+ });
42
+ usePdfPage({ canvas, pageNumber: 1, pdfDocument });
43
+ ```
44
+
45
+ Bind `canvas` to a `<canvas ref="canvas" />` in the component's template, and the page is drawn into it.
46
+
47
+ ## <a name="key-exports">🧩 Key Exports</a>
25
48
 
26
- We highly recommend you take a look at the [documentation](https://github.com/Esposter/Esposter/tree/main/openspec/specs) to level up.
49
+ | Export | Kind | Purpose |
50
+ | ------------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
51
+ | `PdfRenderer` | interface | The renderer's API module, its data folder URLs and its worker, which the consumer supplies |
52
+ | `PdfSource` | type | A document by its URL, its bytes, or a file the reader picked |
53
+ | `PdfViewerState` | interface | The viewer's pages, zoom, rotation, search, download and print, as state and actions |
54
+ | `readXfdf` | function | Reads the free-text annotations of an XFDF file, or an error naming what is wrong with it |
55
+ | `usePdfAnnotations` | composable | The reader's comments and free-text annotations, exported to and imported from XFDF |
56
+ | `usePdfDocument` | composable | Loads a document from a source through the consumer's renderer, with its password prompt and failure as state |
57
+ | `usePdfPage` | composable | Draws one page of a loaded document into a canvas, at the canvas's width and the device's pixel ratio |
58
+ | `usePdfThumbnails` | composable | One thumbnail per page, drawn only while its row is in view of the sidebar |
59
+ | `usePdfViewer` | composable | Builds the renderer's viewer for a loaded document, with its pages, zoom and search as state |
60
+ | `usePdfZoomGesture` | composable | Pinch and Ctrl-wheel zoom the viewer about the pointer, keeping the content under it in place |
61
+ | `writeXfdf` | function | Writes free-text annotations as XFDF |
27
62
 
28
63
  ## <a name="license">⚖️ License</a>
29
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-pdfjs-headless",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "description": "Headless Vue composables over pdfjs-dist: a document's loading, a page's drawing and the viewer's state, with no styling and no renderer of its own.",
5
5
  "keywords": [
6
6
  "headless",
@@ -43,20 +43,21 @@
43
43
  "lint": "eslint .",
44
44
  "lint:fix": "eslint --fix .",
45
45
  "test": "vitest",
46
- "typecheck": "tsc"
46
+ "typecheck": "tsrs --fast"
47
47
  },
48
48
  "dependencies": {
49
- "@esposter/shared": "4.3.1"
49
+ "@esposter/shared": "4.4.0",
50
+ "neverthrow": "^8.2.0"
50
51
  },
51
52
  "devDependencies": {
52
- "@esposter/configuration": "4.3.1",
53
+ "@esposter/configuration": "4.4.0",
53
54
  "@vue/test-utils": "^2.5.1",
54
55
  "happy-dom": "^21.0.4",
55
- "tsdown": "^0.23.1"
56
+ "tsdown": "0.23.2"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "pdfjs-dist": "^6.4.299",
59
60
  "vue": "^3.5.44"
60
61
  },
61
- "gitHead": "777a39636b789c13af33ed13370246ccf6dbaf31"
62
+ "gitHead": "e1a416c4b91da69cc2aa259b50a4d656f204d272"
62
63
  }
package/dist/index.d.ts DELETED
@@ -1,189 +0,0 @@
1
- import { ComputedRef, MaybeRefOrGetter, Ref, ShallowRef } from "vue";
2
- import * as PdfJs from "pdfjs-dist";
3
- import { PDFDocumentLoadingTask, PDFDocumentProxy, PageViewport } from "pdfjs-dist";
4
- import * as PdfJsViewer from "pdfjs-dist/web/pdf_viewer.mjs";
5
- import { PDFViewer } from "pdfjs-dist/web/pdf_viewer.mjs";
6
- //#region src/models/PdfApi.d.ts
7
- export type PdfApi = Pick<typeof PdfJs, "getDocument" | "GlobalWorkerOptions" | "PasswordResponses">;
8
- //#endregion
9
- //#region src/models/PdfDocumentParameters.d.ts
10
- export type PdfDocumentParameters = Partial<Parameters<PdfApi["getDocument"]>[0]>;
11
- //#endregion
12
- //#region src/models/PdfLoadingProgress.d.ts
13
- export interface PdfLoadingProgress {
14
- loaded: number;
15
- total: number;
16
- }
17
- //#endregion
18
- //#region src/models/PdfDocumentState.d.ts
19
- export interface PdfDocumentState {
20
- isFailed: Ref<boolean>;
21
- isPasswordRequested: Ref<boolean>;
22
- isPasswordWrong: Ref<boolean>;
23
- pdfDocument: ShallowRef<PDFDocumentProxy | undefined>;
24
- progress: Ref<PdfLoadingProgress>;
25
- submitPassword: (password: string) => void;
26
- }
27
- //#endregion
28
- //#region src/models/PdfRenderer.d.ts
29
- export interface PdfRenderer {
30
- api: PdfApi;
31
- cMapUrl: string;
32
- standardFontDataUrl: string;
33
- wasmUrl: string;
34
- workerSrc: string;
35
- }
36
- //#endregion
37
- //#region src/models/PdfSource.d.ts
38
- export type PdfSource = File | string | Uint8Array;
39
- //#endregion
40
- //#region src/composables/usePdfDocument.d.ts
41
- interface UsePdfDocumentOptions {
42
- parameters?: PdfDocumentParameters;
43
- renderer: MaybeRefOrGetter<PdfRenderer | undefined>;
44
- source: MaybeRefOrGetter<PdfSource | undefined>;
45
- }
46
- export declare const usePdfDocument: ({ parameters, renderer, source }: UsePdfDocumentOptions) => PdfDocumentState;
47
- //#endregion
48
- //#region src/composables/usePdfPage.d.ts
49
- interface UsePdfPageOptions {
50
- canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>;
51
- isVisible?: MaybeRefOrGetter<boolean>;
52
- pageNumber: MaybeRefOrGetter<number>;
53
- pdfDocument: MaybeRefOrGetter<PDFDocumentProxy | undefined>;
54
- }
55
- export declare const usePdfPage: ({ canvas, isVisible, pageNumber, pdfDocument }: UsePdfPageOptions) => void;
56
- //#endregion
57
- //#region src/models/PdfThumbnail.d.ts
58
- export interface PdfThumbnail {
59
- canvas: ShallowRef<HTMLCanvasElement | undefined>;
60
- isCurrent: ComputedRef<boolean>;
61
- isVisible: Ref<boolean>;
62
- pageNumber: number;
63
- }
64
- //#endregion
65
- //#region src/composables/usePdfThumbnails.d.ts
66
- interface UsePdfThumbnailsOptions {
67
- currentPageNumber: MaybeRefOrGetter<number>;
68
- pdfDocument: MaybeRefOrGetter<PDFDocumentProxy | undefined>;
69
- root: MaybeRefOrGetter<HTMLElement | undefined>;
70
- }
71
- export declare const usePdfThumbnails: ({ currentPageNumber, pdfDocument, root }: UsePdfThumbnailsOptions) => ShallowRef<PdfThumbnail[]>;
72
- //#endregion
73
- //#region src/models/PdfViewerApi.d.ts
74
- export type PdfViewerApi = Pick<typeof PdfJsViewer, "EventBus" | "FindState" | "LinkTarget" | "PDFFindController" | "PDFLinkService" | "PDFViewer">;
75
- //#endregion
76
- //#region src/models/PdfMatchesCount.d.ts
77
- export interface PdfMatchesCount {
78
- current: number;
79
- total: number;
80
- }
81
- //#endregion
82
- //#region src/models/PdfOutline.d.ts
83
- export type PdfOutline = NonNullable<Awaited<ReturnType<PDFDocumentProxy["getOutline"]>>>;
84
- //#endregion
85
- //#region src/models/PdfPageOverlay.d.ts
86
- export interface PdfPageOverlay {
87
- element: HTMLElement;
88
- pageNumber: number;
89
- viewport: PageViewport;
90
- }
91
- //#endregion
92
- //#region src/models/PdfViewerState.d.ts
93
- export interface PdfViewerState {
94
- actualSize: () => void;
95
- currentPageNumber: Ref<number>;
96
- download: (filename: string) => void;
97
- findNext: () => void;
98
- findPrevious: () => void;
99
- fitPage: () => void;
100
- fitWidth: () => void;
101
- followBookmark: (item: PdfOutline[number]) => void;
102
- goToPage: (pageNumber: number) => void;
103
- isFullscreen: Ref<boolean>;
104
- isMatchCase: Ref<boolean>;
105
- isNotFound: Ref<boolean>;
106
- isWholeWord: Ref<boolean>;
107
- isZoomInDisabled: ComputedRef<boolean>;
108
- matchesCount: Ref<PdfMatchesCount>;
109
- nextPage: () => void;
110
- outline: ShallowRef<PdfOutline>;
111
- overlays: ShallowRef<PdfPageOverlay[]>;
112
- pagesCount: Ref<number>;
113
- pagesRotation: Ref<number>;
114
- previousPage: () => void;
115
- print: () => void;
116
- rotate: () => void;
117
- scale: Ref<number>;
118
- searchQuery: Ref<string>;
119
- toggleFullscreen: () => void;
120
- zoomIn: () => void;
121
- zoomOut: () => void;
122
- }
123
- //#endregion
124
- //#region src/composables/usePdfViewer.d.ts
125
- interface UsePdfViewerOptions {
126
- container: MaybeRefOrGetter<HTMLDivElement | undefined>;
127
- highlightTerms?: MaybeRefOrGetter<string[]>;
128
- pdfDocument: MaybeRefOrGetter<PDFDocumentProxy | undefined>;
129
- viewer: MaybeRefOrGetter<HTMLDivElement | undefined>;
130
- viewerApi: MaybeRefOrGetter<PdfViewerApi | undefined>;
131
- }
132
- export declare const usePdfViewer: ({ container, highlightTerms, pdfDocument, viewer, viewerApi }: UsePdfViewerOptions) => PdfViewerState;
133
- //#endregion
134
- //#region src/composables/usePdfZoomGesture.d.ts
135
- interface UsePdfZoomGestureOptions {
136
- container: MaybeRefOrGetter<HTMLDivElement | undefined>;
137
- getViewer: () => PDFViewer | undefined;
138
- }
139
- export declare const usePdfZoomGesture: ({ container, getViewer }: UsePdfZoomGestureOptions) => void;
140
- //#endregion
141
- //#region src/services/constants.d.ts
142
- export declare const PDF_MAX_CANVAS_AREA = 16777216;
143
- export declare const PDF_MIN_SCALE = .1;
144
- export declare const PDF_MAX_SCALE = 10;
145
- export declare const PDF_ROTATION_STEP = 90;
146
- export declare const PDF_FULL_TURN = 360;
147
- export declare const PDF_PRINT_SCALE = 2;
148
- export declare const PDF_PRINT_BATCH_SIZE = 10;
149
- export declare const PDF_PRINT_CLASS = "pdf-print";
150
- export declare const PDF_MIME_TYPE = "application/pdf";
151
- export declare const PDF_PRINT_STYLE: string;
152
- export declare const PDF_WHEEL_ZOOM_DIVISOR = 500;
153
- //#endregion
154
- //#region src/services/destroyLoadingTask.d.ts
155
- export declare const destroyLoadingTask: (task: PDFDocumentLoadingTask) => void;
156
- //#endregion
157
- //#region src/services/getCanvasScale.d.ts
158
- interface Props {
159
- cssWidth: number;
160
- pageHeight: number;
161
- pageWidth: number;
162
- pixelRatio: number;
163
- }
164
- export declare const getCanvasScale: ({ cssWidth, pageHeight, pageWidth, pixelRatio }: Props) => number;
165
- //#endregion
166
- //#region src/services/getSourceParameters.d.ts
167
- export declare const getSourceParameters: (source: PdfSource) => Promise<{
168
- data: Uint8Array;
169
- } | {
170
- url: string;
171
- }>;
172
- //#endregion
173
- //#region src/services/getZoomedScroll.d.ts
174
- export declare const getZoomedScroll: ({ point, ratio, scroll }: {
175
- point: number;
176
- ratio: number;
177
- scroll: number;
178
- }) => number;
179
- //#endregion
180
- //#region src/services/loadPdfDocument.d.ts
181
- export declare const loadPdfDocument: (renderer: PdfRenderer, source: PdfSource, parameters: PdfDocumentParameters) => Promise<PDFDocumentLoadingTask>;
182
- //#endregion
183
- //#region src/services/renderPrintImage.d.ts
184
- export declare const renderPrintImage: (pdfDocument: PDFDocumentProxy, pageNumber: number) => Promise<HTMLImageElement>;
185
- //#endregion
186
- //#region src/services/renderPrintSheets.d.ts
187
- export declare const renderPrintSheets: (pdfDocument: PDFDocumentProxy, checkIsCancelled: () => boolean) => Promise<HTMLDivElement | undefined>;
188
- //#endregion
189
- export {};
package/dist/index.js DELETED
@@ -1,667 +0,0 @@
1
- import { downloadFile, getResultAsync, getSynchronizedFunction, noop } from "@esposter/shared";
2
- import { computed, effectScope, onScopeDispose, ref, shallowRef, toValue, watch } from "vue";
3
- //#region src/services/destroyLoadingTask.ts
4
- const destroyLoadingTask = getSynchronizedFunction(async (task) => {
5
- await task.destroy();
6
- });
7
- //#endregion
8
- //#region src/services/getSourceParameters.ts
9
- const getSourceParameters = async (source) => {
10
- if (typeof source === "string") return { url: source };
11
- if (source instanceof File) return { data: new Uint8Array(await source.arrayBuffer()) };
12
- return { data: new Uint8Array(source) };
13
- };
14
- //#endregion
15
- //#region src/services/loadPdfDocument.ts
16
- const loadPdfDocument = async (renderer, source, parameters) => {
17
- renderer.api.GlobalWorkerOptions.workerSrc = renderer.workerSrc;
18
- const sourceParameters = await getSourceParameters(source);
19
- return renderer.api.getDocument({
20
- cMapPacked: true,
21
- cMapUrl: renderer.cMapUrl,
22
- standardFontDataUrl: renderer.standardFontDataUrl,
23
- wasmUrl: renderer.wasmUrl,
24
- ...parameters,
25
- ...sourceParameters
26
- });
27
- };
28
- //#endregion
29
- //#region src/composables/usePdfDocument.ts
30
- const EMPTY_PROGRESS = {
31
- loaded: 0,
32
- total: 0
33
- };
34
- const usePdfDocument = ({ parameters = {}, renderer, source }) => {
35
- const pdfDocument = shallowRef();
36
- const progress = ref(EMPTY_PROGRESS);
37
- const isFailed = ref(false);
38
- const isPasswordRequested = ref(false);
39
- const isPasswordWrong = ref(false);
40
- let loadingTask;
41
- let updatePassword;
42
- let loadGeneration = 0;
43
- const destroyCurrentTask = () => {
44
- if (loadingTask) destroyLoadingTask(loadingTask);
45
- loadingTask = void 0;
46
- };
47
- const release = () => {
48
- loadGeneration++;
49
- destroyCurrentTask();
50
- pdfDocument.value = void 0;
51
- progress.value = EMPTY_PROGRESS;
52
- isFailed.value = false;
53
- isPasswordRequested.value = false;
54
- isPasswordWrong.value = false;
55
- };
56
- const load = async (currentRenderer, currentSource) => {
57
- const generation = loadGeneration;
58
- await getResultAsync(async () => {
59
- const task = await loadPdfDocument(currentRenderer, currentSource, parameters);
60
- if (generation !== loadGeneration) {
61
- destroyLoadingTask(task);
62
- return;
63
- }
64
- loadingTask = task;
65
- task.onProgress = ({ loaded, total }) => {
66
- if (generation === loadGeneration) progress.value = {
67
- loaded,
68
- total
69
- };
70
- };
71
- task.onPassword = (passwordCallback, reason) => {
72
- if (generation !== loadGeneration) return;
73
- updatePassword = passwordCallback;
74
- isPasswordWrong.value = reason === currentRenderer.api.PasswordResponses.INCORRECT_PASSWORD;
75
- isPasswordRequested.value = true;
76
- };
77
- const loadedDocument = await task.promise;
78
- if (generation === loadGeneration) pdfDocument.value = loadedDocument;
79
- }).match(noop, (error) => {
80
- if (generation !== loadGeneration) return;
81
- isFailed.value = true;
82
- destroyCurrentTask();
83
- console.error(error);
84
- });
85
- };
86
- const submitPassword = (password) => {
87
- isPasswordRequested.value = false;
88
- updatePassword?.(password);
89
- };
90
- watch([() => toValue(renderer), () => toValue(source)], getSynchronizedFunction(async ([currentRenderer, currentSource]) => {
91
- release();
92
- if (currentRenderer && currentSource) await load(currentRenderer, currentSource);
93
- }), { immediate: true });
94
- onScopeDispose(release);
95
- return {
96
- isFailed,
97
- isPasswordRequested,
98
- isPasswordWrong,
99
- pdfDocument,
100
- progress,
101
- submitPassword
102
- };
103
- };
104
- //#endregion
105
- //#region src/services/constants.ts
106
- const PDF_MAX_CANVAS_AREA = 16777216;
107
- const PDF_MIN_SCALE = .1;
108
- const PDF_MAX_SCALE = 10;
109
- const PDF_ROTATION_STEP = 90;
110
- const PDF_FULL_TURN = 360;
111
- const PDF_PRINT_SCALE = 2;
112
- const PDF_PRINT_BATCH_SIZE = 10;
113
- const PDF_PRINT_CLASS = "pdf-print";
114
- const PDF_MIME_TYPE = "application/pdf";
115
- const PDF_PRINT_STYLE = `@page { margin: 3mm } .${PDF_PRINT_CLASS} { display: none } .${PDF_PRINT_CLASS} img { display: block; page-break-after: always; width: 100% } @media print { .${PDF_PRINT_CLASS} { display: block } body > :not(.${PDF_PRINT_CLASS}) { display: none !important } }`;
116
- const PDF_WHEEL_ZOOM_DIVISOR = 500;
117
- //#endregion
118
- //#region src/services/getCanvasScale.ts
119
- const getCanvasScale = ({ cssWidth, pageHeight, pageWidth, pixelRatio }) => {
120
- const scale = cssWidth / pageWidth * pixelRatio;
121
- if (pageWidth * pageHeight * scale ** 2 <= 16777216) return scale;
122
- return Math.sqrt(PDF_MAX_CANVAS_AREA / (pageWidth * pageHeight));
123
- };
124
- //#endregion
125
- //#region src/composables/usePdfPage.ts
126
- const usePdfPage = ({ canvas, isVisible = true, pageNumber, pdfDocument }) => {
127
- let cssWidth = 0;
128
- let loadGeneration = 0;
129
- let page;
130
- let renderTask;
131
- const awaitRender = getSynchronizedFunction(async (task) => {
132
- await getResultAsync(() => task.promise).match(noop, (error) => {
133
- if (task === renderTask) console.error(error);
134
- });
135
- });
136
- const cancelRender = () => {
137
- renderTask?.cancel();
138
- renderTask = void 0;
139
- };
140
- const drawPage = () => {
141
- const currentCanvas = toValue(canvas);
142
- if (!page || !currentCanvas || cssWidth === 0 || !toValue(isVisible)) return;
143
- cancelRender();
144
- const { height, width } = page.getViewport({ scale: 1 });
145
- const viewport = page.getViewport({ scale: getCanvasScale({
146
- cssWidth,
147
- pageHeight: height,
148
- pageWidth: width,
149
- pixelRatio: window.devicePixelRatio
150
- }) });
151
- currentCanvas.width = viewport.width;
152
- currentCanvas.height = viewport.height;
153
- const task = page.render({
154
- canvas: currentCanvas,
155
- viewport
156
- });
157
- renderTask = task;
158
- awaitRender(task);
159
- };
160
- watch([() => toValue(pdfDocument), () => toValue(pageNumber)], getSynchronizedFunction(async ([currentDocument, currentPageNumber]) => {
161
- const generation = ++loadGeneration;
162
- cancelRender();
163
- page = void 0;
164
- if (!currentDocument) return;
165
- await getResultAsync(async () => {
166
- const loadedPage = await currentDocument.getPage(currentPageNumber);
167
- if (generation !== loadGeneration) return;
168
- page = loadedPage;
169
- drawPage();
170
- }).match(noop, (error) => {
171
- if (generation === loadGeneration) console.error(error);
172
- });
173
- }), { immediate: true });
174
- watch(() => toValue(canvas), (currentCanvas, _previousCanvas, onCleanup) => {
175
- if (!currentCanvas) {
176
- cancelRender();
177
- return;
178
- }
179
- cssWidth = 0;
180
- const observer = new ResizeObserver((entries) => {
181
- const width = entries[0]?.contentRect.width ?? 0;
182
- if (width === cssWidth) return;
183
- cssWidth = width;
184
- drawPage();
185
- });
186
- observer.observe(currentCanvas);
187
- onCleanup(() => {
188
- observer.disconnect();
189
- });
190
- }, { immediate: true });
191
- watch(() => toValue(isVisible), (visible) => {
192
- if (visible) drawPage();
193
- });
194
- onScopeDispose(() => {
195
- loadGeneration++;
196
- cancelRender();
197
- });
198
- };
199
- //#endregion
200
- //#region src/composables/usePdfThumbnails.ts
201
- const usePdfThumbnails = ({ currentPageNumber, pdfDocument, root }) => {
202
- const thumbnails = shallowRef([]);
203
- const rows = /* @__PURE__ */ new Map();
204
- let observer;
205
- let scope;
206
- const createThumbnail = (pageNumber, currentDocument, currentObserver) => {
207
- const canvas = shallowRef();
208
- const isVisible = ref(false);
209
- const thumbnail = {
210
- canvas,
211
- isCurrent: computed(() => toValue(currentPageNumber) === pageNumber),
212
- isVisible,
213
- pageNumber
214
- };
215
- usePdfPage({
216
- canvas: () => canvas.value,
217
- isVisible: () => isVisible.value,
218
- pageNumber,
219
- pdfDocument: currentDocument
220
- });
221
- watch(canvas, (element, _previousElement, onCleanup) => {
222
- if (!element) return;
223
- rows.set(element, thumbnail);
224
- currentObserver.observe(element);
225
- onCleanup(() => {
226
- currentObserver.unobserve(element);
227
- rows.delete(element);
228
- });
229
- });
230
- return thumbnail;
231
- };
232
- const disposeThumbnails = () => {
233
- scope?.stop();
234
- observer?.disconnect();
235
- };
236
- watch(() => toValue(pdfDocument), (loaded) => {
237
- disposeThumbnails();
238
- rows.clear();
239
- thumbnails.value = [];
240
- if (!loaded) return;
241
- const currentObserver = new IntersectionObserver((entries) => {
242
- for (const entry of entries) {
243
- const row = rows.get(entry.target);
244
- if (row) row.isVisible.value = entry.isIntersecting;
245
- }
246
- }, { root: toValue(root) ?? null });
247
- observer = currentObserver;
248
- scope = effectScope();
249
- thumbnails.value = scope.run(() => Array.from({ length: loaded.numPages }, (_value, index) => createThumbnail(index + 1, loaded, currentObserver))) ?? [];
250
- }, { immediate: true });
251
- onScopeDispose(disposeThumbnails);
252
- return thumbnails;
253
- };
254
- //#endregion
255
- //#region src/services/getZoomedScroll.ts
256
- const getZoomedScroll = ({ point, ratio, scroll }) => (scroll + point) * ratio - point;
257
- //#endregion
258
- //#region src/composables/usePdfZoomGesture.ts
259
- const usePdfZoomGesture = ({ container, getViewer }) => {
260
- const abortController = new AbortController();
261
- const pointers = /* @__PURE__ */ new Map();
262
- let pinchDistance = 0;
263
- const measurePinch = () => {
264
- const [first, second] = [...pointers.values()];
265
- if (pointers.size !== 2 || !first || !second) return void 0;
266
- return {
267
- distance: Math.hypot(second.x - first.x, second.y - first.y),
268
- point: {
269
- x: (first.x + second.x) / 2,
270
- y: (first.y + second.y) / 2
271
- }
272
- };
273
- };
274
- const measurePinchDistance = () => measurePinch()?.distance ?? 0;
275
- const zoomAbout = (ratio, point) => {
276
- const viewer = getViewer();
277
- const element = toValue(container);
278
- if (!viewer || !element) return;
279
- const { currentScale } = viewer;
280
- const nextScale = Math.min(Math.max(currentScale * ratio, PDF_MIN_SCALE), 10);
281
- const scaleRatio = nextScale / currentScale;
282
- const { left, top } = element.getBoundingClientRect();
283
- const { scrollLeft, scrollTop } = element;
284
- viewer.currentScale = nextScale;
285
- element.scrollLeft = getZoomedScroll({
286
- point: point.x - left,
287
- ratio: scaleRatio,
288
- scroll: scrollLeft
289
- });
290
- element.scrollTop = getZoomedScroll({
291
- point: point.y - top,
292
- ratio: scaleRatio,
293
- scroll: scrollTop
294
- });
295
- };
296
- const onPointerDown = (event) => {
297
- pointers.set(event.pointerId, {
298
- x: event.clientX,
299
- y: event.clientY
300
- });
301
- pinchDistance = measurePinchDistance();
302
- };
303
- const onPointerMove = (event) => {
304
- if (!pointers.has(event.pointerId)) return;
305
- pointers.set(event.pointerId, {
306
- x: event.clientX,
307
- y: event.clientY
308
- });
309
- const pinch = measurePinch();
310
- if (pinch && pinchDistance > 0) zoomAbout(pinch.distance / pinchDistance, pinch.point);
311
- pinchDistance = pinch?.distance ?? 0;
312
- };
313
- const onPointerEnd = (event) => {
314
- pointers.delete(event.pointerId);
315
- pinchDistance = measurePinchDistance();
316
- };
317
- const onWheel = (event) => {
318
- if (!event.ctrlKey) return;
319
- event.preventDefault();
320
- zoomAbout(Math.exp(-event.deltaY / 500), {
321
- x: event.clientX,
322
- y: event.clientY
323
- });
324
- };
325
- watch(() => toValue(container), (element) => {
326
- if (!element) return;
327
- const { signal } = abortController;
328
- element.addEventListener("pointerdown", (event) => {
329
- onPointerDown(event);
330
- }, { signal });
331
- element.addEventListener("pointermove", (event) => {
332
- onPointerMove(event);
333
- }, { signal });
334
- element.addEventListener("pointerup", (event) => {
335
- onPointerEnd(event);
336
- }, { signal });
337
- element.addEventListener("pointercancel", (event) => {
338
- onPointerEnd(event);
339
- }, { signal });
340
- element.addEventListener("pointerleave", (event) => {
341
- onPointerEnd(event);
342
- }, { signal });
343
- element.addEventListener("wheel", (event) => {
344
- onWheel(event);
345
- }, {
346
- passive: false,
347
- signal
348
- });
349
- }, { immediate: true });
350
- onScopeDispose(() => {
351
- abortController.abort();
352
- });
353
- };
354
- //#endregion
355
- //#region src/services/renderPrintImage.ts
356
- const renderPrintImage = async (pdfDocument, pageNumber) => {
357
- const page = await pdfDocument.getPage(pageNumber);
358
- const { height, width } = page.getViewport({ scale: 1 });
359
- const printScale = getCanvasScale({
360
- cssWidth: width,
361
- pageHeight: height,
362
- pageWidth: width,
363
- pixelRatio: 2
364
- });
365
- const viewport = page.getViewport({ scale: printScale });
366
- const canvas = window.document.createElement("canvas");
367
- canvas.width = viewport.width;
368
- canvas.height = viewport.height;
369
- await page.render({
370
- canvas,
371
- viewport
372
- }).promise;
373
- const image = window.document.createElement("img");
374
- image.src = canvas.toDataURL("image/png");
375
- return image;
376
- };
377
- //#endregion
378
- //#region src/services/renderPrintSheets.ts
379
- const renderPrintSheets = async (pdfDocument, checkIsCancelled) => {
380
- const style = window.document.createElement("style");
381
- style.textContent = PDF_PRINT_STYLE;
382
- const printRoot = window.document.createElement("div");
383
- printRoot.className = PDF_PRINT_CLASS;
384
- printRoot.append(style);
385
- const renderBatch = async (firstPageNumber) => {
386
- if (checkIsCancelled() || firstPageNumber > pdfDocument.numPages) return;
387
- const lastPageNumber = Math.min(firstPageNumber + 10 - 1, pdfDocument.numPages);
388
- const pageNumbers = Array.from({ length: lastPageNumber - firstPageNumber + 1 }, (_value, index) => firstPageNumber + index);
389
- printRoot.append(...await Promise.all(pageNumbers.map((pageNumber) => renderPrintImage(pdfDocument, pageNumber))));
390
- await renderBatch(lastPageNumber + 1);
391
- };
392
- await renderBatch(1);
393
- return checkIsCancelled() ? void 0 : printRoot;
394
- };
395
- //#endregion
396
- //#region src/composables/usePdfViewer.ts
397
- const usePdfViewer = ({ container, highlightTerms = [], pdfDocument, viewer, viewerApi }) => {
398
- const currentPageNumber = ref(0);
399
- const isFullscreen = ref(false);
400
- const isMatchCase = ref(false);
401
- const isNotFound = ref(false);
402
- const isWholeWord = ref(false);
403
- const matchesCount = ref({
404
- current: 0,
405
- total: 0
406
- });
407
- const outline = shallowRef([]);
408
- const overlays = shallowRef([]);
409
- const pagesCount = ref(0);
410
- const pagesRotation = ref(0);
411
- const scale = ref(0);
412
- const searchQuery = ref("");
413
- const isZoomInDisabled = computed(() => scale.value >= 10);
414
- let abortController;
415
- let containerElement;
416
- let eventBus;
417
- let linkService;
418
- let pdfViewer;
419
- let resizeObserver;
420
- let printRoot;
421
- let printGeneration = 0;
422
- const onFullscreenChange = () => {
423
- isFullscreen.value = window.document.fullscreenElement === containerElement;
424
- };
425
- const createViewer = (currentViewerApi, containerValue, viewerElement) => {
426
- const { EventBus, FindState, LinkTarget, PDFFindController, PDFLinkService, PDFViewer } = currentViewerApi;
427
- const bus = new EventBus();
428
- const links = new PDFLinkService({
429
- eventBus: bus,
430
- externalLinkTarget: LinkTarget.BLANK
431
- });
432
- const controller = new PDFFindController({
433
- eventBus: bus,
434
- linkService: links
435
- });
436
- const viewerAbortController = new AbortController();
437
- const { signal } = viewerAbortController;
438
- const instance = new PDFViewer({
439
- abortSignal: signal,
440
- container: containerValue,
441
- eventBus: bus,
442
- findController: controller,
443
- linkService: links,
444
- maxCanvasPixels: PDF_MAX_CANVAS_AREA,
445
- viewer: viewerElement
446
- });
447
- links.setViewer(instance);
448
- abortController = viewerAbortController;
449
- containerElement = containerValue;
450
- eventBus = bus;
451
- linkService = links;
452
- pdfViewer = instance;
453
- bus.on("pagesinit", () => {
454
- instance.currentScaleValue = "auto";
455
- });
456
- bus.on("pagechanging", (event) => {
457
- currentPageNumber.value = event.pageNumber;
458
- });
459
- bus.on("pagerendered", (event) => {
460
- const overlay = {
461
- element: event.source.div,
462
- pageNumber: event.pageNumber,
463
- viewport: event.source.viewport
464
- };
465
- overlays.value = [...overlays.value.filter(({ pageNumber }) => pageNumber !== event.pageNumber), overlay];
466
- });
467
- bus.on("scalechanging", (event) => {
468
- scale.value = event.scale;
469
- });
470
- bus.on("rotationchanging", (event) => {
471
- pagesRotation.value = event.pagesRotation;
472
- });
473
- bus.on("updatefindcontrolstate", (event) => {
474
- isNotFound.value = event.state === FindState.NOT_FOUND;
475
- matchesCount.value = event.matchesCount;
476
- });
477
- bus.on("updatefindmatchescount", (event) => {
478
- matchesCount.value = event.matchesCount;
479
- });
480
- resizeObserver = new ResizeObserver(() => {
481
- if (!pdfViewer) return;
482
- const { currentScaleValue } = pdfViewer;
483
- if (currentScaleValue) pdfViewer.currentScaleValue = currentScaleValue;
484
- });
485
- resizeObserver.observe(containerValue);
486
- window.document.addEventListener("fullscreenchange", () => {
487
- onFullscreenChange();
488
- }, { signal });
489
- window.addEventListener("afterprint", () => {
490
- removePrint();
491
- }, { signal });
492
- applyDocument(toValue(pdfDocument));
493
- };
494
- const loadOutline = getSynchronizedFunction(async (loaded) => {
495
- await getResultAsync(async () => {
496
- const items = await loaded.getOutline();
497
- if (loaded === toValue(pdfDocument)) outline.value = items ?? [];
498
- }).match(noop, console.error);
499
- });
500
- const applyDocument = (loaded) => {
501
- if (!pdfViewer || !linkService) return;
502
- pagesCount.value = loaded?.numPages ?? 0;
503
- overlays.value = [];
504
- outline.value = [];
505
- pdfViewer.setDocument(loaded ?? null);
506
- linkService.setDocument(loaded ?? null);
507
- currentPageNumber.value = 1;
508
- pagesRotation.value = 0;
509
- matchesCount.value = {
510
- current: 0,
511
- total: 0
512
- };
513
- isNotFound.value = false;
514
- if (!loaded) return;
515
- loadOutline(loaded);
516
- const terms = toValue(highlightTerms);
517
- if (terms.length > 0) findText({ query: terms });
518
- };
519
- const findText = ({ isPrevious = false, query, type = "" }) => {
520
- eventBus?.dispatch("find", {
521
- caseSensitive: isMatchCase.value,
522
- entireWord: isWholeWord.value,
523
- findPrevious: isPrevious,
524
- highlightAll: true,
525
- matchDiacritics: false,
526
- query,
527
- type
528
- });
529
- };
530
- const removePrint = () => {
531
- printGeneration++;
532
- printRoot?.remove();
533
- printRoot = void 0;
534
- };
535
- const followBookmark = (item) => {
536
- if (item.dest) linkService?.goToDestination(item.dest);
537
- };
538
- const goToPage = (pageNumber) => {
539
- if (pdfViewer && Number.isInteger(pageNumber) && pageNumber >= 1 && pageNumber <= pagesCount.value) pdfViewer.currentPageNumber = pageNumber;
540
- };
541
- const previousPage = () => {
542
- pdfViewer?.previousPage();
543
- };
544
- const nextPage = () => {
545
- pdfViewer?.nextPage();
546
- };
547
- const zoomIn = () => {
548
- pdfViewer?.increaseScale();
549
- };
550
- const zoomOut = () => {
551
- pdfViewer?.decreaseScale();
552
- };
553
- const fitWidth = () => {
554
- if (pdfViewer) pdfViewer.currentScaleValue = "page-width";
555
- };
556
- const fitPage = () => {
557
- if (pdfViewer) pdfViewer.currentScaleValue = "page-fit";
558
- };
559
- const actualSize = () => {
560
- if (pdfViewer) pdfViewer.currentScaleValue = "page-actual";
561
- };
562
- const rotate = () => {
563
- if (pdfViewer) pdfViewer.pagesRotation = (pdfViewer.pagesRotation + 90) % 360;
564
- };
565
- const findNext = () => {
566
- findText({
567
- query: searchQuery.value,
568
- type: "again"
569
- });
570
- };
571
- const findPrevious = () => {
572
- findText({
573
- isPrevious: true,
574
- query: searchQuery.value,
575
- type: "again"
576
- });
577
- };
578
- const toggleFullscreen = getSynchronizedFunction(async () => {
579
- const element = containerElement;
580
- if (!element) return;
581
- await getResultAsync(async () => {
582
- if (window.document.fullscreenElement) await window.document.exitFullscreen();
583
- else await element.requestFullscreen();
584
- }).match(noop, console.error);
585
- });
586
- const download = getSynchronizedFunction(async (filename) => {
587
- const downloaded = toValue(pdfDocument);
588
- if (!downloaded) return;
589
- await getResultAsync(async () => {
590
- downloadFile(filename, new Uint8Array(await downloaded.getData()), PDF_MIME_TYPE);
591
- }).match(noop, console.error);
592
- });
593
- const print = getSynchronizedFunction(async () => {
594
- const printed = toValue(pdfDocument);
595
- if (!printed) return;
596
- removePrint();
597
- const generation = printGeneration;
598
- await getResultAsync(async () => {
599
- const sheets = await renderPrintSheets(printed, () => generation !== printGeneration);
600
- if (!sheets) return;
601
- window.document.body.append(sheets);
602
- printRoot = sheets;
603
- window.print();
604
- }).match(noop, console.error);
605
- });
606
- usePdfZoomGesture({
607
- container,
608
- getViewer: () => pdfViewer
609
- });
610
- watch([
611
- () => toValue(viewerApi),
612
- () => toValue(container),
613
- () => toValue(viewer)
614
- ], ([currentViewerApi, containerElementValue, viewerElement]) => {
615
- if (pdfViewer || !currentViewerApi || !containerElementValue || !viewerElement) return;
616
- createViewer(currentViewerApi, containerElementValue, viewerElement);
617
- }, { immediate: true });
618
- watch(() => toValue(pdfDocument), (loaded) => {
619
- applyDocument(loaded);
620
- });
621
- watch([
622
- searchQuery,
623
- isMatchCase,
624
- isWholeWord
625
- ], () => {
626
- findText({ query: searchQuery.value });
627
- });
628
- onScopeDispose(() => {
629
- removePrint();
630
- if (!pdfViewer) return;
631
- pdfViewer.setDocument(null);
632
- abortController?.abort();
633
- resizeObserver?.disconnect();
634
- });
635
- return {
636
- actualSize,
637
- currentPageNumber,
638
- download,
639
- findNext,
640
- findPrevious,
641
- fitPage,
642
- fitWidth,
643
- followBookmark,
644
- goToPage,
645
- isFullscreen,
646
- isMatchCase,
647
- isNotFound,
648
- isWholeWord,
649
- isZoomInDisabled,
650
- matchesCount,
651
- nextPage,
652
- outline,
653
- overlays,
654
- pagesCount,
655
- pagesRotation,
656
- previousPage,
657
- print,
658
- rotate,
659
- scale,
660
- searchQuery,
661
- toggleFullscreen,
662
- zoomIn,
663
- zoomOut
664
- };
665
- };
666
- //#endregion
667
- export { PDF_FULL_TURN, PDF_MAX_CANVAS_AREA, PDF_MAX_SCALE, PDF_MIME_TYPE, PDF_MIN_SCALE, PDF_PRINT_BATCH_SIZE, PDF_PRINT_CLASS, PDF_PRINT_SCALE, PDF_PRINT_STYLE, PDF_ROTATION_STEP, PDF_WHEEL_ZOOM_DIVISOR, destroyLoadingTask, getCanvasScale, getSourceParameters, getZoomedScroll, loadPdfDocument, renderPrintImage, renderPrintSheets, usePdfDocument, usePdfPage, usePdfThumbnails, usePdfViewer, usePdfZoomGesture };