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