svelte-pdf-view 0.1.13 → 0.3.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.
@@ -0,0 +1,437 @@
1
+ /* Copyright 2024 Mozilla Foundation
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+ export var PresentationModeState;
16
+ (function (PresentationModeState) {
17
+ PresentationModeState[PresentationModeState["UNKNOWN"] = 0] = "UNKNOWN";
18
+ PresentationModeState[PresentationModeState["NORMAL"] = 1] = "NORMAL";
19
+ PresentationModeState[PresentationModeState["CHANGING"] = 2] = "CHANGING";
20
+ PresentationModeState[PresentationModeState["FULLSCREEN"] = 3] = "FULLSCREEN";
21
+ })(PresentationModeState || (PresentationModeState = {}));
22
+ const MOUSE_SCROLL_COOLDOWN_TIME = 50; // in ms
23
+ const PAGE_SWITCH_THRESHOLD = 0.1;
24
+ const SWIPE_MIN_DISTANCE_THRESHOLD = 50;
25
+ const SWIPE_ANGLE_THRESHOLD = Math.PI / 6;
26
+ export class PdfPresentationMode {
27
+ state = PresentationModeState.UNKNOWN;
28
+ pdfDocument = null;
29
+ currentPageNumber = 1;
30
+ totalPages = 0;
31
+ container = null;
32
+ canvas = null;
33
+ callbacks;
34
+ fullscreenChangeAbortController = null;
35
+ windowAbortController = null;
36
+ mouseScrollTimeStamp = 0;
37
+ mouseScrollDelta = 0;
38
+ touchSwipeState = null;
39
+ renderingPage = false;
40
+ constructor(callbacks = {}) {
41
+ this.callbacks = callbacks;
42
+ }
43
+ /**
44
+ * Set the PDF document for presentation
45
+ */
46
+ setDocument(pdfDocument) {
47
+ this.pdfDocument = pdfDocument;
48
+ this.totalPages = pdfDocument?.numPages ?? 0;
49
+ }
50
+ /**
51
+ * Set the current page number (used when entering presentation mode)
52
+ */
53
+ setCurrentPage(pageNumber) {
54
+ this.currentPageNumber = Math.max(1, Math.min(pageNumber, this.totalPages));
55
+ }
56
+ /**
57
+ * Check if presentation mode is active
58
+ */
59
+ get active() {
60
+ return (this.state === PresentationModeState.CHANGING ||
61
+ this.state === PresentationModeState.FULLSCREEN);
62
+ }
63
+ /**
64
+ * Get current state
65
+ */
66
+ get currentState() {
67
+ return this.state;
68
+ }
69
+ /**
70
+ * Request entering fullscreen presentation mode
71
+ */
72
+ async request() {
73
+ if (this.active || !this.pdfDocument || this.totalPages === 0) {
74
+ return false;
75
+ }
76
+ // Create the presentation container
77
+ this.createPresentationContainer();
78
+ if (!this.container) {
79
+ return false;
80
+ }
81
+ this.addFullscreenChangeListeners();
82
+ this.notifyStateChange(PresentationModeState.CHANGING);
83
+ try {
84
+ await this.container.requestFullscreen();
85
+ return true;
86
+ }
87
+ catch {
88
+ this.removeFullscreenChangeListeners();
89
+ this.notifyStateChange(PresentationModeState.NORMAL);
90
+ this.destroyPresentationContainer();
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Exit presentation mode
96
+ */
97
+ async exit() {
98
+ if (!this.active) {
99
+ return;
100
+ }
101
+ if (document.fullscreenElement) {
102
+ await document.exitFullscreen();
103
+ }
104
+ }
105
+ /**
106
+ * Go to next page
107
+ */
108
+ nextPage() {
109
+ if (this.currentPageNumber >= this.totalPages) {
110
+ return false;
111
+ }
112
+ this.currentPageNumber++;
113
+ this.renderCurrentPage();
114
+ this.callbacks.onPageChange?.(this.currentPageNumber);
115
+ return true;
116
+ }
117
+ /**
118
+ * Go to previous page
119
+ */
120
+ previousPage() {
121
+ if (this.currentPageNumber <= 1) {
122
+ return false;
123
+ }
124
+ this.currentPageNumber--;
125
+ this.renderCurrentPage();
126
+ this.callbacks.onPageChange?.(this.currentPageNumber);
127
+ return true;
128
+ }
129
+ /**
130
+ * Go to a specific page
131
+ */
132
+ goToPage(pageNumber) {
133
+ if (pageNumber < 1 || pageNumber > this.totalPages) {
134
+ return false;
135
+ }
136
+ this.currentPageNumber = pageNumber;
137
+ this.renderCurrentPage();
138
+ this.callbacks.onPageChange?.(this.currentPageNumber);
139
+ return true;
140
+ }
141
+ /**
142
+ * Destroy and cleanup
143
+ */
144
+ destroy() {
145
+ this.exit();
146
+ this.removeWindowListeners();
147
+ this.removeFullscreenChangeListeners();
148
+ this.destroyPresentationContainer();
149
+ }
150
+ // Private methods
151
+ createPresentationContainer() {
152
+ // Create fullscreen container
153
+ this.container = document.createElement('div');
154
+ this.container.className = 'pdf-presentation-mode';
155
+ this.container.style.cssText = `
156
+ position: fixed;
157
+ top: 0;
158
+ left: 0;
159
+ width: 100%;
160
+ height: 100%;
161
+ background-color: #000;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ z-index: 999999;
166
+ `;
167
+ // Create canvas for rendering
168
+ this.canvas = document.createElement('canvas');
169
+ this.canvas.style.cssText = `
170
+ max-width: 100%;
171
+ max-height: 100%;
172
+ object-fit: contain;
173
+ `;
174
+ this.container.appendChild(this.canvas);
175
+ document.body.appendChild(this.container);
176
+ }
177
+ destroyPresentationContainer() {
178
+ if (this.container) {
179
+ this.container.remove();
180
+ this.container = null;
181
+ this.canvas = null;
182
+ }
183
+ }
184
+ async renderCurrentPage() {
185
+ if (!this.pdfDocument || !this.canvas || !this.container || this.renderingPage) {
186
+ return;
187
+ }
188
+ this.renderingPage = true;
189
+ try {
190
+ const page = await this.pdfDocument.getPage(this.currentPageNumber);
191
+ // Calculate scale to fit the screen while maintaining aspect ratio
192
+ const containerWidth = window.innerWidth;
193
+ const containerHeight = window.innerHeight;
194
+ const viewport = page.getViewport({ scale: 1, rotation: 0 });
195
+ const pageWidth = viewport.width;
196
+ const pageHeight = viewport.height;
197
+ // Calculate scale to fit
198
+ const scaleX = containerWidth / pageWidth;
199
+ const scaleY = containerHeight / pageHeight;
200
+ const scale = Math.min(scaleX, scaleY);
201
+ const scaledViewport = page.getViewport({ scale, rotation: 0 });
202
+ // Set canvas size
203
+ this.canvas.width = scaledViewport.width;
204
+ this.canvas.height = scaledViewport.height;
205
+ const context = this.canvas.getContext('2d');
206
+ if (!context) {
207
+ return;
208
+ }
209
+ // Clear and render
210
+ context.fillStyle = '#fff';
211
+ context.fillRect(0, 0, this.canvas.width, this.canvas.height);
212
+ await page.render({
213
+ canvasContext: context,
214
+ viewport: scaledViewport,
215
+ canvas: this.canvas
216
+ }).promise;
217
+ }
218
+ catch (e) {
219
+ console.error('Failed to render presentation page:', e);
220
+ }
221
+ finally {
222
+ this.renderingPage = false;
223
+ }
224
+ }
225
+ notifyStateChange(newState) {
226
+ this.state = newState;
227
+ this.callbacks.onStateChange?.(newState);
228
+ }
229
+ enter() {
230
+ this.notifyStateChange(PresentationModeState.FULLSCREEN);
231
+ this.addWindowListeners();
232
+ this.renderCurrentPage();
233
+ // Clear any text selection
234
+ document.getSelection()?.empty();
235
+ }
236
+ doExit() {
237
+ this.removeWindowListeners();
238
+ this.destroyPresentationContainer();
239
+ this.resetMouseScrollState();
240
+ this.removeFullscreenChangeListeners();
241
+ this.notifyStateChange(PresentationModeState.NORMAL);
242
+ }
243
+ handleMouseWheel = (evt) => {
244
+ if (!this.active) {
245
+ return;
246
+ }
247
+ evt.preventDefault();
248
+ const delta = this.normalizeWheelDelta(evt);
249
+ const currentTime = Date.now();
250
+ const storedTime = this.mouseScrollTimeStamp;
251
+ // Cooldown to prevent accidental double-switching
252
+ if (currentTime > storedTime && currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) {
253
+ return;
254
+ }
255
+ // Reset if direction changed
256
+ if ((this.mouseScrollDelta > 0 && delta < 0) || (this.mouseScrollDelta < 0 && delta > 0)) {
257
+ this.resetMouseScrollState();
258
+ }
259
+ this.mouseScrollDelta += delta;
260
+ if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) {
261
+ const totalDelta = this.mouseScrollDelta;
262
+ this.resetMouseScrollState();
263
+ const success = totalDelta > 0 ? this.previousPage() : this.nextPage();
264
+ if (success) {
265
+ this.mouseScrollTimeStamp = currentTime;
266
+ }
267
+ }
268
+ };
269
+ normalizeWheelDelta(evt) {
270
+ let delta = Math.hypot(evt.deltaX, evt.deltaY);
271
+ const angle = Math.atan2(evt.deltaY, evt.deltaX);
272
+ if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
273
+ delta = -delta;
274
+ }
275
+ if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
276
+ delta *= 30;
277
+ }
278
+ else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
279
+ delta *= 30 * 10;
280
+ }
281
+ return delta / 30;
282
+ }
283
+ handleMouseDown = (evt) => {
284
+ // Left click (0) = next page, Right click (2) = previous page
285
+ if (evt.button === 0) {
286
+ evt.preventDefault();
287
+ if (evt.shiftKey) {
288
+ this.previousPage();
289
+ }
290
+ else {
291
+ this.nextPage();
292
+ }
293
+ }
294
+ else if (evt.button === 2) {
295
+ evt.preventDefault();
296
+ this.previousPage();
297
+ }
298
+ };
299
+ handleKeyDown = (evt) => {
300
+ if (!this.active) {
301
+ return;
302
+ }
303
+ this.resetMouseScrollState();
304
+ switch (evt.key) {
305
+ case 'ArrowRight':
306
+ case 'ArrowDown':
307
+ case ' ':
308
+ case 'PageDown':
309
+ case 'Enter':
310
+ evt.preventDefault();
311
+ this.nextPage();
312
+ break;
313
+ case 'ArrowLeft':
314
+ case 'ArrowUp':
315
+ case 'PageUp':
316
+ case 'Backspace':
317
+ evt.preventDefault();
318
+ this.previousPage();
319
+ break;
320
+ case 'Home':
321
+ evt.preventDefault();
322
+ this.goToPage(1);
323
+ break;
324
+ case 'End':
325
+ evt.preventDefault();
326
+ this.goToPage(this.totalPages);
327
+ break;
328
+ case 'Escape':
329
+ // Escape is handled by the browser for fullscreen exit
330
+ break;
331
+ }
332
+ };
333
+ handleContextMenu = (evt) => {
334
+ // Prevent context menu - right-click is handled in handleMouseDown
335
+ evt.preventDefault();
336
+ };
337
+ handleTouchSwipe = (evt) => {
338
+ if (!this.active) {
339
+ return;
340
+ }
341
+ if (evt.touches.length > 1) {
342
+ this.touchSwipeState = null;
343
+ return;
344
+ }
345
+ switch (evt.type) {
346
+ case 'touchstart':
347
+ this.touchSwipeState = {
348
+ startX: evt.touches[0].pageX,
349
+ startY: evt.touches[0].pageY,
350
+ endX: evt.touches[0].pageX,
351
+ endY: evt.touches[0].pageY
352
+ };
353
+ break;
354
+ case 'touchmove':
355
+ if (this.touchSwipeState === null) {
356
+ return;
357
+ }
358
+ this.touchSwipeState.endX = evt.touches[0].pageX;
359
+ this.touchSwipeState.endY = evt.touches[0].pageY;
360
+ evt.preventDefault();
361
+ break;
362
+ case 'touchend': {
363
+ if (this.touchSwipeState === null) {
364
+ return;
365
+ }
366
+ const dx = this.touchSwipeState.endX - this.touchSwipeState.startX;
367
+ const dy = this.touchSwipeState.endY - this.touchSwipeState.startY;
368
+ const absAngle = Math.abs(Math.atan2(dy, dx));
369
+ let delta = 0;
370
+ if (Math.abs(dx) > SWIPE_MIN_DISTANCE_THRESHOLD &&
371
+ (absAngle <= SWIPE_ANGLE_THRESHOLD || absAngle >= Math.PI - SWIPE_ANGLE_THRESHOLD)) {
372
+ // Horizontal swipe
373
+ delta = dx;
374
+ }
375
+ else if (Math.abs(dy) > SWIPE_MIN_DISTANCE_THRESHOLD &&
376
+ Math.abs(absAngle - Math.PI / 2) <= SWIPE_ANGLE_THRESHOLD) {
377
+ // Vertical swipe
378
+ delta = dy;
379
+ }
380
+ if (delta > 0) {
381
+ this.previousPage();
382
+ }
383
+ else if (delta < 0) {
384
+ this.nextPage();
385
+ }
386
+ this.touchSwipeState = null;
387
+ break;
388
+ }
389
+ }
390
+ };
391
+ handleResize = () => {
392
+ if (this.active) {
393
+ this.renderCurrentPage();
394
+ }
395
+ };
396
+ resetMouseScrollState() {
397
+ this.mouseScrollTimeStamp = 0;
398
+ this.mouseScrollDelta = 0;
399
+ }
400
+ addWindowListeners() {
401
+ if (this.windowAbortController) {
402
+ return;
403
+ }
404
+ this.windowAbortController = new AbortController();
405
+ const { signal } = this.windowAbortController;
406
+ window.addEventListener('mousedown', this.handleMouseDown, { signal });
407
+ window.addEventListener('wheel', this.handleMouseWheel, { passive: false, signal });
408
+ window.addEventListener('keydown', this.handleKeyDown, { signal });
409
+ window.addEventListener('contextmenu', this.handleContextMenu, { signal });
410
+ window.addEventListener('touchstart', this.handleTouchSwipe, { signal });
411
+ window.addEventListener('touchmove', this.handleTouchSwipe, { passive: false, signal });
412
+ window.addEventListener('touchend', this.handleTouchSwipe, { signal });
413
+ window.addEventListener('resize', this.handleResize, { signal });
414
+ }
415
+ removeWindowListeners() {
416
+ this.windowAbortController?.abort();
417
+ this.windowAbortController = null;
418
+ }
419
+ addFullscreenChangeListeners() {
420
+ if (this.fullscreenChangeAbortController) {
421
+ return;
422
+ }
423
+ this.fullscreenChangeAbortController = new AbortController();
424
+ window.addEventListener('fullscreenchange', () => {
425
+ if (document.fullscreenElement) {
426
+ this.enter();
427
+ }
428
+ else {
429
+ this.doExit();
430
+ }
431
+ }, { signal: this.fullscreenChangeAbortController.signal });
432
+ }
433
+ removeFullscreenChangeListeners() {
434
+ this.fullscreenChangeAbortController?.abort();
435
+ this.fullscreenChangeAbortController = null;
436
+ }
437
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * SimpleLinkService - Provides PDF navigation for annotation links.
3
+ * Adapted from PDF.js pdf_link_service.js
4
+ */
5
+ import type { PDFDocumentProxy } from 'pdfjs-dist/legacy/build/pdf.mjs';
6
+ import type { EventBus } from './EventBus.js';
7
+ export interface SimpleLinkServiceOptions {
8
+ eventBus: EventBus;
9
+ externalLinkTarget?: number;
10
+ externalLinkRel?: string;
11
+ }
12
+ export declare const LinkTarget: {
13
+ readonly NONE: 0;
14
+ readonly SELF: 1;
15
+ readonly BLANK: 2;
16
+ readonly PARENT: 3;
17
+ readonly TOP: 4;
18
+ };
19
+ export declare class SimpleLinkService {
20
+ readonly eventBus: EventBus;
21
+ private externalLinkTarget;
22
+ private externalLinkRel;
23
+ private pdfDocument;
24
+ private pdfViewer;
25
+ externalLinkEnabled: boolean;
26
+ constructor(options: SimpleLinkServiceOptions);
27
+ setDocument(pdfDocument: PDFDocumentProxy | null): void;
28
+ setViewer(pdfViewer: {
29
+ scrollToPage: (page: number) => void;
30
+ pagesCount: number;
31
+ }): void;
32
+ get pagesCount(): number;
33
+ get page(): number;
34
+ set page(value: number);
35
+ get rotation(): number;
36
+ set rotation(_value: number);
37
+ get isInPresentationMode(): boolean;
38
+ /**
39
+ * Navigate to a PDF destination (internal link).
40
+ */
41
+ goToDestination(dest: unknown): Promise<void>;
42
+ /**
43
+ * Navigate to a specific page.
44
+ */
45
+ goToPage(pageNumber: number): void;
46
+ /**
47
+ * Get a hash string for a destination (for href).
48
+ */
49
+ getDestinationHash(dest: unknown): string;
50
+ /**
51
+ * Get anchor URL with proper prefix.
52
+ */
53
+ getAnchorUrl(anchor: string): string;
54
+ /**
55
+ * Add attributes to external link elements.
56
+ */
57
+ addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow?: boolean): void;
58
+ /**
59
+ * Execute a named action (e.g., Print, GoBack).
60
+ */
61
+ executeNamedAction(action: string): void;
62
+ /**
63
+ * Execute a SetOCGState action.
64
+ */
65
+ executeSetOCGState(_action: unknown): void;
66
+ /**
67
+ * Navigate to specific coordinates on a page.
68
+ */
69
+ goToXY(pageNumber: number, _x: number, _y: number): void;
70
+ /**
71
+ * Set hash for navigation.
72
+ */
73
+ setHash(_hash: string): void;
74
+ }