genus-pdf-viewer 0.1.13 → 0.2.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.
@@ -1,1346 +0,0 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, signal, effect, ViewChild, Input, Component, InjectionToken, PLATFORM_ID, APP_INITIALIZER } from '@angular/core';
3
- import * as i1 from '@angular/common';
4
- import { CommonModule, isPlatformBrowser } from '@angular/common';
5
- import { DomSanitizer } from '@angular/platform-browser';
6
- import * as pdfjsLib from 'pdfjs-dist';
7
-
8
- class GenusPdfViewerComponent {
9
- static inlineWorkerSetupPromise = null;
10
- sanitizer = inject(DomSanitizer);
11
- src;
12
- page = 1;
13
- zoom = 1.0;
14
- maxZoom = 4;
15
- minZoom = 0.25;
16
- fit = 'width';
17
- disableTextLayer = true;
18
- continuous = true;
19
- showToolbar = true;
20
- zoomAnimation = true;
21
- zoomAnimationMs = 180;
22
- gestureAnimationMs = 120;
23
- loadTimeoutMs = 45000;
24
- allowIframeFallback = true;
25
- withCredentials = false;
26
- httpHeaders;
27
- canvas;
28
- singleStage;
29
- stage;
30
- doc;
31
- currentPage;
32
- pageSig = signal(1, ...(ngDevMode ? [{ debugName: "pageSig" }] : []));
33
- zoomSig = signal(1, ...(ngDevMode ? [{ debugName: "zoomSig" }] : []));
34
- isPanning = signal(false, ...(ngDevMode ? [{ debugName: "isPanning" }] : []));
35
- canPan = signal(false, ...(ngDevMode ? [{ debugName: "canPan" }] : []));
36
- loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
37
- error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
38
- iframeFallbackSrc = signal(null, ...(ngDevMode ? [{ debugName: "iframeFallbackSrc" }] : []));
39
- PRINT_SCALE = 2;
40
- scrollRaf = 0;
41
- renderLockCount = 0;
42
- zoomAnimInFlight = false;
43
- activeRenderTasks = new Set();
44
- wheelRaf = 0;
45
- pinchState = null;
46
- activePointers = new Map();
47
- renderVersion = 0;
48
- previewActive = false;
49
- reRenderQueued = false;
50
- pendingAnchor;
51
- lastZoomClient = null;
52
- WHEEL_STEP = 0.02;
53
- PINCH_SENSITIVITY = 0.5;
54
- wheelCommitTimer = 0;
55
- lastLoadError = null;
56
- constructor() {
57
- effect(() => {
58
- // Only react to zoom changes here; page changes in continuous mode should NOT trigger a re-render
59
- this.zoomSig();
60
- if (this.renderLockCount > 0 || this.previewActive)
61
- return;
62
- this.safeRender();
63
- });
64
- }
65
- async ngOnChanges(changes) {
66
- if (changes['page'])
67
- this.pageSig.set(this.page);
68
- if (changes['zoom'])
69
- this.zoomSig.set(this.zoom);
70
- const srcChanged = !!changes['src'];
71
- const layoutChanged = !!(changes['fit'] || changes['minZoom'] || changes['maxZoom']);
72
- if (!this.src)
73
- return;
74
- if (srcChanged) {
75
- await this.loadDocument();
76
- if (this.doc) {
77
- const pages = this.doc.numPages;
78
- if (this.pageSig() < 1)
79
- this.pageSig.set(1);
80
- if (this.pageSig() > pages)
81
- this.pageSig.set(pages);
82
- await this.render();
83
- }
84
- return;
85
- }
86
- if (layoutChanged) {
87
- if (this.continuous) {
88
- const container = this.stage?.nativeElement;
89
- const anchor = container ? this.captureScrollAnchor(container) || undefined : undefined;
90
- await this.renderAllPages(anchor);
91
- }
92
- else {
93
- await this.render();
94
- }
95
- return;
96
- }
97
- if (!this.continuous && changes['page']) {
98
- await this.render();
99
- }
100
- }
101
- async safeRender() {
102
- try {
103
- await this.render();
104
- }
105
- catch (_) { }
106
- }
107
- async loadDocument() {
108
- this.loading.set(true);
109
- this.error.set(null);
110
- this.iframeFallbackSrc.set(null);
111
- this.lastLoadError = null;
112
- if (this.doc) {
113
- try {
114
- await this.doc.destroy();
115
- }
116
- catch { }
117
- this.doc = undefined;
118
- }
119
- try {
120
- this.doc = await this.loadDocumentWithFallbacks(this.src);
121
- if (!this.doc) {
122
- if (!this.tryEnableIframeFallback()) {
123
- this.error.set(this.buildLoadErrorMessage());
124
- }
125
- }
126
- }
127
- catch {
128
- this.doc = undefined;
129
- if (!this.tryEnableIframeFallback()) {
130
- this.error.set(this.buildLoadErrorMessage());
131
- }
132
- }
133
- finally {
134
- this.loading.set(false);
135
- }
136
- }
137
- async loadDocumentWithFallbacks(src) {
138
- await this.ensureInlineWorkerFallback();
139
- const workerCandidates = this.getWorkerSrcCandidates();
140
- const baseTimeout = this.getEffectiveLoadTimeoutMs();
141
- const primary = this.createPrimaryParams(src);
142
- if (primary && workerCandidates.length) {
143
- for (let i = 0; i < workerCandidates.length; i++) {
144
- this.applyWorkerSrc(workerCandidates[i]);
145
- const timeoutMs = i === 0 ? baseTimeout : Math.min(baseTimeout, 12000);
146
- const doc = await this.tryLoadDocument(primary, timeoutMs);
147
- if (doc)
148
- return doc;
149
- }
150
- }
151
- else if (primary) {
152
- const doc = await this.tryLoadDocument(primary, baseTimeout);
153
- if (doc)
154
- return doc;
155
- }
156
- const bytes = await this.toPdfBytes(src);
157
- if (!bytes)
158
- return undefined;
159
- const byteParams = {
160
- data: bytes,
161
- disableRange: true,
162
- disableStream: true,
163
- disableAutoFetch: true,
164
- stopAtErrors: true,
165
- };
166
- if (workerCandidates.length) {
167
- for (let i = 0; i < workerCandidates.length; i++) {
168
- this.applyWorkerSrc(workerCandidates[i]);
169
- const timeoutMs = i === 0 ? baseTimeout : Math.min(baseTimeout, 12000);
170
- const doc = await this.tryLoadDocument(byteParams, timeoutMs);
171
- if (doc)
172
- return doc;
173
- }
174
- return undefined;
175
- }
176
- return this.tryLoadDocument(byteParams, baseTimeout);
177
- }
178
- createPrimaryParams(src) {
179
- if (typeof src === 'string') {
180
- return {
181
- url: src,
182
- withCredentials: this.withCredentials,
183
- httpHeaders: this.httpHeaders,
184
- stopAtErrors: true,
185
- };
186
- }
187
- if (src instanceof URL) {
188
- return {
189
- url: src.toString(),
190
- withCredentials: this.withCredentials,
191
- httpHeaders: this.httpHeaders,
192
- stopAtErrors: true,
193
- };
194
- }
195
- if (src instanceof Uint8Array) {
196
- const copy = new Uint8Array(src.byteLength);
197
- copy.set(src);
198
- return {
199
- data: copy,
200
- disableRange: true,
201
- disableStream: true,
202
- disableAutoFetch: true,
203
- stopAtErrors: true,
204
- };
205
- }
206
- return null;
207
- }
208
- async tryLoadDocument(params, timeoutMs) {
209
- const task = pdfjsLib.getDocument(params);
210
- let timer = null;
211
- try {
212
- if (timeoutMs <= 0) {
213
- return await task.promise;
214
- }
215
- const timeoutPromise = new Promise((_, reject) => {
216
- timer = setTimeout(() => reject(new Error('PDF load timeout')), timeoutMs);
217
- });
218
- return await Promise.race([task.promise, timeoutPromise]);
219
- }
220
- catch (e) {
221
- this.lastLoadError = e;
222
- try {
223
- await task.destroy();
224
- }
225
- catch { }
226
- return undefined;
227
- }
228
- finally {
229
- if (timer)
230
- clearTimeout(timer);
231
- }
232
- }
233
- getEffectiveLoadTimeoutMs() {
234
- if (!Number.isFinite(this.loadTimeoutMs))
235
- return 45000;
236
- if (this.loadTimeoutMs <= 0)
237
- return 0;
238
- return Math.max(5000, this.loadTimeoutMs);
239
- }
240
- async ensureInlineWorkerFallback() {
241
- if (typeof window === 'undefined')
242
- return;
243
- const g = globalThis;
244
- if (g?.pdfjsWorker?.WorkerMessageHandler)
245
- return;
246
- if (!GenusPdfViewerComponent.inlineWorkerSetupPromise) {
247
- GenusPdfViewerComponent.inlineWorkerSetupPromise = (async () => {
248
- try {
249
- // @ts-ignore pdfjs-dist worker entry does not ship declaration files.
250
- const workerModule = await import('pdfjs-dist/build/pdf.worker.min.mjs');
251
- if (workerModule?.WorkerMessageHandler) {
252
- g.pdfjsWorker = { ...(g.pdfjsWorker || {}), WorkerMessageHandler: workerModule.WorkerMessageHandler };
253
- }
254
- }
255
- catch { }
256
- })();
257
- }
258
- await GenusPdfViewerComponent.inlineWorkerSetupPromise;
259
- }
260
- resolveBundledWorkerSrc() {
261
- try {
262
- return new URL('../workers/pdf.worker.min.mjs', import.meta.url).toString();
263
- }
264
- catch {
265
- return null;
266
- }
267
- }
268
- getWorkerSrcCandidates() {
269
- const options = pdfjsLib.GlobalWorkerOptions || {};
270
- const current = typeof options.workerSrc === 'string' ? options.workerSrc : '';
271
- const bundled = this.resolveBundledWorkerSrc() || '';
272
- const assetJs = '/assets/pdf.worker.min.js';
273
- const assetMjs = '/assets/pdf.worker.min.mjs';
274
- const candidates = [bundled, current, assetJs, assetMjs].filter((v) => !!v);
275
- return Array.from(new Set(candidates));
276
- }
277
- applyWorkerSrc(workerSrc) {
278
- const options = pdfjsLib.GlobalWorkerOptions || {};
279
- options.workerPort = null;
280
- options.workerSrc = workerSrc;
281
- }
282
- buildLoadErrorMessage() {
283
- const raw = this.extractErrorText(this.lastLoadError).trim();
284
- const lower = raw.toLowerCase();
285
- if (lower.includes('worker')) {
286
- return 'PDF worker bootstrap failed. Check CSP/network restrictions for dynamic imports and retry.';
287
- }
288
- if (lower.includes('cors') || lower.includes('network') || lower.includes('fetch')) {
289
- return 'PDF request failed. Check file URL, network, and CORS policy, then retry.';
290
- }
291
- if (lower.includes('password')) {
292
- return 'This PDF appears to be password protected.';
293
- }
294
- return 'Failed to load PDF. Check URL/CORS and worker setup, then retry.';
295
- }
296
- tryEnableIframeFallback() {
297
- if (!this.allowIframeFallback)
298
- return false;
299
- if (!this.isLikelyNetworkOrCorsError())
300
- return false;
301
- const href = this.toFallbackHref(this.src);
302
- if (!href)
303
- return false;
304
- this.iframeFallbackSrc.set(this.sanitizer.bypassSecurityTrustResourceUrl(href));
305
- this.error.set(null);
306
- this.pageSig.set(1);
307
- return true;
308
- }
309
- isLikelyNetworkOrCorsError() {
310
- const lower = this.extractErrorText(this.lastLoadError).toLowerCase();
311
- return (lower.includes('cors') ||
312
- lower.includes('network') ||
313
- lower.includes('fetch') ||
314
- lower.includes('load failed') ||
315
- lower.includes('failed to fetch') ||
316
- lower.includes('securityerror'));
317
- }
318
- toFallbackHref(src) {
319
- if (typeof window === 'undefined')
320
- return null;
321
- if (typeof src === 'string') {
322
- try {
323
- const url = new URL(src, window.location.href);
324
- if (!['http:', 'https:', 'blob:', 'data:'].includes(url.protocol))
325
- return null;
326
- return url.toString();
327
- }
328
- catch {
329
- return null;
330
- }
331
- }
332
- if (src instanceof URL) {
333
- if (!['http:', 'https:', 'blob:', 'data:'].includes(src.protocol))
334
- return null;
335
- return src.toString();
336
- }
337
- return null;
338
- }
339
- extractErrorText(error) {
340
- if (!error)
341
- return '';
342
- if (typeof error === 'string')
343
- return error;
344
- if (typeof error === 'object') {
345
- const maybeMessage = error.message;
346
- if (typeof maybeMessage === 'string')
347
- return maybeMessage;
348
- try {
349
- return JSON.stringify(error);
350
- }
351
- catch {
352
- return String(error);
353
- }
354
- }
355
- return String(error);
356
- }
357
- async toPdfBytes(src) {
358
- if (src instanceof Uint8Array) {
359
- const copy = new Uint8Array(src.byteLength);
360
- copy.set(src);
361
- return copy;
362
- }
363
- if (src instanceof Blob) {
364
- try {
365
- return new Uint8Array(await src.arrayBuffer());
366
- }
367
- catch {
368
- return null;
369
- }
370
- }
371
- const href = typeof src === 'string' ? src : src.toString();
372
- if (typeof fetch === 'undefined')
373
- return null;
374
- try {
375
- const res = await fetch(href, {
376
- mode: 'cors',
377
- headers: this.httpHeaders,
378
- credentials: this.withCredentials ? 'include' : 'same-origin',
379
- });
380
- if (!res.ok)
381
- return null;
382
- return new Uint8Array(await res.arrayBuffer());
383
- }
384
- catch {
385
- return null;
386
- }
387
- }
388
- async render() {
389
- if (!this.doc)
390
- return;
391
- const version = ++this.renderVersion;
392
- if (this.continuous) {
393
- await this.renderAllPages();
394
- return;
395
- }
396
- const pageNum = this.pageSig();
397
- const pdfPage = await this.doc.getPage(pageNum);
398
- this.currentPage = pdfPage;
399
- const canvas = this.canvas?.nativeElement;
400
- if (!canvas)
401
- return;
402
- const ctx = canvas.getContext('2d');
403
- const baseViewport = pdfPage.getViewport({ scale: 1 });
404
- const parent = canvas.parentElement;
405
- const anchor = this.captureScrollAnchor(parent) || undefined;
406
- const pW = parent.clientWidth, pH = parent.clientHeight || baseViewport.height;
407
- let baseScale = 1;
408
- if (this.fit === 'width')
409
- baseScale = (pW - 16) / baseViewport.width;
410
- if (this.fit === 'height')
411
- baseScale = (pH - 16) / baseViewport.height;
412
- if (this.fit === 'page')
413
- baseScale = Math.min((pW - 16) / baseViewport.width, (pH - 16) / baseViewport.height);
414
- const minScale = baseScale * this.minZoom;
415
- const maxScale = baseScale * this.maxZoom;
416
- const finalScale = Math.max(minScale, Math.min(maxScale, baseScale * this.zoomSig()));
417
- this.canPan.set(finalScale > baseScale + 0.001);
418
- const viewport = pdfPage.getViewport({ scale: finalScale });
419
- const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
420
- canvas.style.width = Math.floor(viewport.width) + 'px';
421
- canvas.style.height = Math.floor(viewport.height) + 'px';
422
- canvas.width = Math.floor(viewport.width * dpr);
423
- canvas.height = Math.floor(viewport.height * dpr);
424
- const renderCtx = { canvasContext: ctx, viewport };
425
- if (dpr !== 1)
426
- renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
427
- const task = pdfPage.render(renderCtx);
428
- await task.promise;
429
- if (this.renderVersion !== version)
430
- return;
431
- if (anchor)
432
- this.restoreScrollAnchor(parent, anchor);
433
- }
434
- async renderAllPages(anchorOverride) {
435
- if (!this.doc)
436
- return;
437
- const stageEl = this.stage?.nativeElement;
438
- if (!stageEl)
439
- return;
440
- const version = ++this.renderVersion;
441
- const anchor = anchorOverride ?? this.captureScrollAnchor(stageEl);
442
- const total = this.doc.numPages;
443
- const parentRect = stageEl.getBoundingClientRect();
444
- const parentWidth = parentRect.width || stageEl.clientWidth || 800;
445
- let computedCanPan = false;
446
- const existing = Array.from(stageEl.querySelectorAll('.pdf-page'));
447
- const isInitialBuild = existing.length !== total;
448
- this.renderLockCount++;
449
- try {
450
- if (isInitialBuild) {
451
- stageEl.innerHTML = '';
452
- const contentEl = document.createElement('div');
453
- contentEl.className = 'pdf-content';
454
- stageEl.appendChild(contentEl);
455
- for (let i = 1; i <= total; i++) {
456
- const page = await this.doc.getPage(i);
457
- const baseViewport = page.getViewport({ scale: 1 });
458
- let baseScale = 1;
459
- if (this.fit === 'width')
460
- baseScale = (parentWidth - 16) / baseViewport.width;
461
- if (this.fit === 'page')
462
- baseScale = (parentWidth - 16) / baseViewport.width;
463
- const minScale = baseScale * this.minZoom;
464
- const maxScale = baseScale * this.maxZoom;
465
- const finalScale = Math.max(minScale, Math.min(maxScale, baseScale * this.zoomSig()));
466
- if (i === 1)
467
- computedCanPan = finalScale > baseScale + 0.001;
468
- const viewport = page.getViewport({ scale: finalScale });
469
- const wrapper = document.createElement('div');
470
- wrapper.className = 'pdf-page';
471
- wrapper.setAttribute('data-page', String(i));
472
- wrapper.style.width = Math.floor(viewport.width) + 'px';
473
- wrapper.style.height = Math.floor(viewport.height) + 'px';
474
- wrapper.style.margin = '0 auto 12px auto';
475
- const canvas = document.createElement('canvas');
476
- const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
477
- canvas.style.width = Math.floor(viewport.width) + 'px';
478
- canvas.style.height = Math.floor(viewport.height) + 'px';
479
- canvas.width = Math.floor(viewport.width * dpr);
480
- canvas.height = Math.floor(viewport.height * dpr);
481
- const ctx = canvas.getContext('2d');
482
- wrapper.appendChild(canvas);
483
- contentEl.appendChild(wrapper);
484
- const renderCtx = { canvasContext: ctx, viewport };
485
- if (dpr !== 1)
486
- renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
487
- const task = page.render(renderCtx);
488
- await task.promise;
489
- if (this.renderVersion !== version)
490
- return;
491
- }
492
- }
493
- else {
494
- let contentEl = stageEl.querySelector('.pdf-content');
495
- if (!contentEl) {
496
- contentEl = document.createElement('div');
497
- contentEl.className = 'pdf-content';
498
- const pages = Array.from(stageEl.querySelectorAll('.pdf-page'));
499
- for (const el of pages)
500
- contentEl.appendChild(el);
501
- stageEl.appendChild(contentEl);
502
- }
503
- for (let i = 1; i <= total; i++) {
504
- const page = await this.doc.getPage(i);
505
- const baseViewport = page.getViewport({ scale: 1 });
506
- let baseScale = 1;
507
- if (this.fit === 'width')
508
- baseScale = (parentWidth - 16) / baseViewport.width;
509
- if (this.fit === 'page')
510
- baseScale = (parentWidth - 16) / baseViewport.width;
511
- const minScale = baseScale * this.minZoom;
512
- const maxScale = baseScale * this.maxZoom;
513
- const finalScale = Math.max(minScale, Math.min(maxScale, baseScale * this.zoomSig()));
514
- if (i === 1)
515
- computedCanPan = finalScale > baseScale + 0.001;
516
- const viewport = page.getViewport({ scale: finalScale });
517
- const wrapper = existing[i - 1];
518
- const canvas = wrapper.querySelector('canvas');
519
- wrapper.style.width = Math.floor(viewport.width) + 'px';
520
- wrapper.style.height = Math.floor(viewport.height) + 'px';
521
- const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
522
- canvas.style.width = Math.floor(viewport.width) + 'px';
523
- canvas.style.height = Math.floor(viewport.height) + 'px';
524
- canvas.width = Math.floor(viewport.width * dpr);
525
- canvas.height = Math.floor(viewport.height * dpr);
526
- const ctx = canvas.getContext('2d');
527
- const renderCtx = { canvasContext: ctx, viewport };
528
- if (dpr !== 1)
529
- renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
530
- const task = page.render(renderCtx);
531
- await task.promise;
532
- if (this.renderVersion !== version)
533
- return;
534
- }
535
- }
536
- this.canPan.set(computedCanPan);
537
- if (anchor)
538
- this.restoreScrollAnchor(stageEl, anchor);
539
- }
540
- finally {
541
- this.renderLockCount = Math.max(0, this.renderLockCount - 1);
542
- }
543
- }
544
- async goTo(page) {
545
- if (!this.doc)
546
- return;
547
- const p = Math.max(1, Math.min(this.doc.numPages, page));
548
- this.pageSig.set(p);
549
- if (this.continuous) {
550
- const stageEl = this.stage?.nativeElement;
551
- const el = stageEl?.querySelector(`[data-page="${p}"]`);
552
- el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
553
- }
554
- else {
555
- await this.render();
556
- }
557
- }
558
- async zoomIn(step = 0.1) { await this.applyZoomDelta(step); }
559
- async zoomOut(step = 0.1) { await this.applyZoomDelta(-step); }
560
- clampZoom(z) {
561
- return Math.max(this.minZoom, Math.min(this.maxZoom, z));
562
- }
563
- cancelPreviewIfAny(container) {
564
- if (!this.previewActive || !container)
565
- return;
566
- const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
567
- try {
568
- target.style.transform = '';
569
- target.style.willChange = '';
570
- target.style.transition = '';
571
- }
572
- catch { }
573
- this.previewActive = false;
574
- this.lastZoomClient = null;
575
- }
576
- async applyZoomDelta(delta) {
577
- const oldZoom = this.zoomSig();
578
- const newZoom = this.clampZoom(oldZoom + delta);
579
- if (Math.abs(newZoom - oldZoom) < 0.0001)
580
- return;
581
- const container = this.continuous ? this.stage?.nativeElement : this.singleStage?.nativeElement;
582
- // If a preview transform is active (from pinch/wheel), clear it before capturing anchor or animating
583
- this.cancelPreviewIfAny(container);
584
- const anchor = container ? this.captureScrollAnchor(container) || undefined : undefined;
585
- const ratio = newZoom / oldZoom;
586
- if (this.zoomAnimation && container && !this.zoomAnimInFlight) {
587
- this.zoomAnimInFlight = true;
588
- this.renderLockCount++;
589
- // Update UI value immediately
590
- this.zoomSig.set(newZoom);
591
- try {
592
- await this.runZoomAnimation(container, ratio);
593
- }
594
- finally {
595
- // Release the render lock before triggering the actual re-render
596
- this.renderLockCount = Math.max(0, this.renderLockCount - 1);
597
- await this.safeReRenderWithCancellation(anchor);
598
- this.zoomAnimInFlight = false;
599
- }
600
- }
601
- else {
602
- this.zoomSig.set(newZoom);
603
- await this.safeReRenderWithCancellation(anchor);
604
- }
605
- }
606
- async runZoomAnimation(container, ratio) {
607
- if (Math.abs(ratio - 1) < 0.0001)
608
- return;
609
- const { target, originX, originY } = this.getZoomAnimTarget(container);
610
- return new Promise((resolve) => {
611
- try {
612
- const onDone = () => {
613
- cleanup();
614
- resolve();
615
- };
616
- const cleanup = () => {
617
- target.removeEventListener('transitionend', onEnd);
618
- target.style.transition = '';
619
- target.style.transform = '';
620
- target.style.transformOrigin = '';
621
- target.style.willChange = '';
622
- };
623
- const onEnd = () => onDone();
624
- target.style.willChange = 'transform';
625
- target.style.transformOrigin = `${Math.max(0, originX)}px ${Math.max(0, originY)}px`;
626
- target.style.transition = 'none';
627
- target.style.transform = 'scale(1)';
628
- requestAnimationFrame(() => {
629
- target.addEventListener('transitionend', onEnd, { once: true });
630
- target.style.transition = `transform ${this.zoomAnimationMs}ms cubic-bezier(0.2,0,0,1)`;
631
- target.style.transform = `scale(${ratio})`;
632
- setTimeout(onDone, this.zoomAnimationMs + 60);
633
- });
634
- }
635
- catch {
636
- resolve();
637
- }
638
- });
639
- }
640
- getZoomAnimTarget(container) {
641
- const isContinuous = this.continuous;
642
- let target = null;
643
- if (isContinuous) {
644
- target = container.querySelector('.pdf-content');
645
- if (!target)
646
- target = container; // fallback
647
- }
648
- else {
649
- target = container.querySelector('canvas');
650
- if (!target)
651
- target = container; // fallback
652
- }
653
- const rectLeft = target.offsetLeft;
654
- const rectTop = target.offsetTop;
655
- // Prefer last gesture point (pinch center or wheel pointer) if available
656
- const client = this.lastZoomClient;
657
- const originX = client ? (container.scrollLeft + client.x - rectLeft) : (container.scrollLeft + container.clientWidth / 2 - rectLeft);
658
- const originY = client ? (container.scrollTop + client.y - rectTop) : (container.scrollTop + container.clientHeight / 2 - rectTop);
659
- return { target, originX, originY };
660
- }
661
- onScroll(evt) {
662
- const container = evt.target;
663
- // debounce via rAF
664
- if (this.scrollRaf)
665
- cancelAnimationFrame(this.scrollRaf);
666
- this.scrollRaf = requestAnimationFrame(() => {
667
- if (!this.doc || !this.continuous)
668
- return;
669
- const pages = Array.from(container.querySelectorAll('.pdf-page'));
670
- if (!pages.length)
671
- return;
672
- const centerY = container.scrollTop + container.clientHeight / 2;
673
- let current = this.pageSig();
674
- for (const el of pages) {
675
- const top = el.offsetTop;
676
- const bottom = top + el.offsetHeight;
677
- if (centerY >= top && centerY <= bottom) {
678
- const p = parseInt(el.getAttribute('data-page') || '1', 10);
679
- current = p;
680
- break;
681
- }
682
- }
683
- if (current !== this.pageSig())
684
- this.pageSig.set(current);
685
- });
686
- }
687
- async nextPage() { await this.goTo(this.pageSig() + 1); }
688
- async prevPage() { await this.goTo(this.pageSig() - 1); }
689
- pageCount() {
690
- return this.doc?.numPages ?? (this.iframeFallbackSrc() ? 1 : 0);
691
- }
692
- isZoomed() {
693
- return Math.abs(this.zoomSig() - 1) > 0.0001;
694
- }
695
- async resetZoom() {
696
- // Reset to exactly 1.0 (100%)
697
- const delta = 1 - this.zoomSig();
698
- await this.applyZoomDelta(delta);
699
- }
700
- async retryLoad() {
701
- await this.loadDocument();
702
- if (this.doc) {
703
- const pages = this.doc.numPages;
704
- if (this.pageSig() < 1)
705
- this.pageSig.set(1);
706
- if (this.pageSig() > pages)
707
- this.pageSig.set(pages);
708
- await this.render();
709
- }
710
- }
711
- onPointerDown(evt, containerEl) {
712
- // Pinch-to-zoom (two pointers)
713
- if (evt.pointerType === 'touch') {
714
- this.activePointers.set(evt.pointerId, { x: evt.clientX, y: evt.clientY, type: evt.pointerType });
715
- const onPointerMove = (e) => {
716
- if (e.pointerType === 'touch')
717
- this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY, type: e.pointerType });
718
- const touches = Array.from(this.activePointers.entries()).map(([id, p]) => ({ pointerId: id, clientX: p.x, clientY: p.y }))
719
- .slice(0, 2);
720
- if (touches.length === 2) {
721
- const [a, b] = touches;
722
- const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
723
- if (!this.pinchState) {
724
- this.pinchState = { id1: a.pointerId, id2: b.pointerId, startDist: Math.max(1, dist), startZoom: this.zoomSig() };
725
- const rect = containerEl.getBoundingClientRect();
726
- this.lastZoomClient = { x: ((a.clientX + b.clientX) / 2) - rect.left, y: ((a.clientY + b.clientY) / 2) - rect.top };
727
- }
728
- const scale = dist / Math.max(1, this.pinchState.startDist);
729
- const adjusted = 1 + (scale - 1) * this.PINCH_SENSITIVITY;
730
- const desiredAdj = this.clampZoom(this.pinchState.startZoom * adjusted);
731
- this.previewZoom(containerEl, desiredAdj);
732
- e.preventDefault();
733
- return;
734
- }
735
- // When pinch ends (not exactly two touches), commit render if we were previewing
736
- if (this.pinchState) {
737
- const final = this.zoomSig();
738
- this.commitPreviewZoom(containerEl, final);
739
- this.pinchState = null;
740
- this.lastZoomClient = null;
741
- }
742
- };
743
- const onPointerEnd = (e) => {
744
- this.activePointers.delete(e.pointerId);
745
- const touches = Array.from(this.activePointers.values());
746
- if (touches.length !== 2 && this.pinchState) {
747
- const final = this.zoomSig();
748
- this.commitPreviewZoom(containerEl, final);
749
- this.pinchState = null;
750
- this.lastZoomClient = null;
751
- }
752
- cleanup();
753
- };
754
- const cleanup = () => {
755
- containerEl.removeEventListener('pointermove', onPointerMove);
756
- containerEl.removeEventListener('pointerup', onPointerEnd);
757
- containerEl.removeEventListener('pointercancel', onPointerEnd);
758
- this.activePointers.clear();
759
- };
760
- containerEl.addEventListener('pointermove', onPointerMove);
761
- containerEl.addEventListener('pointerup', onPointerEnd);
762
- containerEl.addEventListener('pointercancel', onPointerEnd);
763
- return;
764
- }
765
- // Mouse pan when canPan
766
- if (!this.canPan())
767
- return;
768
- this.isPanning.set(true);
769
- const startX = evt.clientX;
770
- const startY = evt.clientY;
771
- const startLeft = containerEl.scrollLeft;
772
- const startTop = containerEl.scrollTop;
773
- containerEl.style.scrollBehavior = 'auto';
774
- containerEl.setPointerCapture?.(evt.pointerId);
775
- const move = (e) => {
776
- const dx = e.clientX - startX;
777
- const dy = e.clientY - startY;
778
- containerEl.scrollLeft = startLeft - dx;
779
- containerEl.scrollTop = startTop - dy;
780
- e.preventDefault();
781
- };
782
- const end = (e) => {
783
- this.isPanning.set(false);
784
- containerEl.releasePointerCapture?.(evt.pointerId);
785
- containerEl.removeEventListener('pointermove', move);
786
- containerEl.removeEventListener('pointerup', end);
787
- containerEl.removeEventListener('pointercancel', end);
788
- containerEl.style.scrollBehavior = 'smooth';
789
- };
790
- containerEl.addEventListener('pointermove', move);
791
- containerEl.addEventListener('pointerup', end);
792
- containerEl.addEventListener('pointercancel', end);
793
- }
794
- getActiveTouches(containerEl) {
795
- // We capture pointer events only on this element; maintain list by querying PointerEvent.getCoalescedEvents not available broadly.
796
- // Instead, rely on touches reported in native events; browsers will deliver move events for all active pointers targeting the element.
797
- // Here we cannot query directly; so we return an empty array and use move handler state. For simplicity, we derive from event stream only.
798
- // This helper remains for future extension; current logic passes actual events to compute pinch.
799
- return [];
800
- }
801
- onWheel(evt, containerEl) {
802
- const isCtrlZoom = evt.ctrlKey || evt.metaKey;
803
- if (!isCtrlZoom)
804
- return;
805
- evt.preventDefault();
806
- const rect = containerEl.getBoundingClientRect();
807
- // Record gesture point relative to container
808
- this.lastZoomClient = { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
809
- if (this.wheelRaf)
810
- cancelAnimationFrame(this.wheelRaf);
811
- if (this.wheelCommitTimer)
812
- clearTimeout(this.wheelCommitTimer);
813
- const delta = -evt.deltaY;
814
- const step = delta > 0 ? this.WHEEL_STEP : -this.WHEEL_STEP;
815
- const desired = this.clampZoom(this.zoomSig() + step);
816
- this.previewZoom(containerEl, desired);
817
- this.wheelRaf = requestAnimationFrame(() => {
818
- this.wheelCommitTimer = setTimeout(() => {
819
- this.commitPreviewZoom(containerEl, this.zoomSig());
820
- this.lastZoomClient = null;
821
- }, this.gestureAnimationMs + 20);
822
- });
823
- }
824
- previewZoom(container, newZoom) {
825
- const oldZoom = this.zoomSig();
826
- if (Math.abs(newZoom - oldZoom) < 0.0001)
827
- return;
828
- const ratio = newZoom / oldZoom;
829
- const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
830
- const origin = this.getZoomAnimTarget(container);
831
- target.style.willChange = 'transform';
832
- target.style.transformOrigin = `${Math.max(0, origin.originX)}px ${Math.max(0, origin.originY)}px`;
833
- target.style.transition = `transform ${this.gestureAnimationMs}ms cubic-bezier(0.2,0,0,1)`;
834
- target.style.transform = `scale(${ratio})`;
835
- this.zoomSig.set(newZoom);
836
- this.previewActive = true;
837
- }
838
- async commitPreviewZoom(container, expectedZoom) {
839
- // Clear transform and re-render once; cancel any in-flight render tasks
840
- const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
841
- target.style.transform = '';
842
- target.style.willChange = '';
843
- target.style.transition = '';
844
- const anchor = this.captureScrollAnchor(container) || undefined;
845
- if (this.previewActive) {
846
- await this.safeReRenderWithCancellation(anchor);
847
- this.previewActive = false;
848
- }
849
- }
850
- async safeReRenderWithCancellation(anchor) {
851
- // If a render is in progress, queue once with the most recent anchor
852
- if (this.renderLockCount > 0) {
853
- this.reRenderQueued = true;
854
- if (anchor)
855
- this.pendingAnchor = anchor;
856
- return;
857
- }
858
- try {
859
- this.renderLockCount++;
860
- if (this.continuous) {
861
- await this.renderAllPages(anchor);
862
- }
863
- else {
864
- await this.render();
865
- }
866
- }
867
- finally {
868
- this.renderLockCount = Math.max(0, this.renderLockCount - 1);
869
- if (this.renderLockCount === 0 && this.reRenderQueued) {
870
- const nextAnchor = this.pendingAnchor;
871
- this.reRenderQueued = false;
872
- this.pendingAnchor = undefined;
873
- await this.safeReRenderWithCancellation(nextAnchor);
874
- }
875
- }
876
- }
877
- captureScrollAnchor(container, client) {
878
- try {
879
- const pages = Array.from(container.querySelectorAll('.pdf-page'));
880
- const centerY = container.scrollTop + container.clientHeight / 2;
881
- if (pages.length > 0) {
882
- let pageEl = pages.find(el => (el.offsetTop <= centerY && (el.offsetTop + el.offsetHeight) >= centerY))
883
- || pages.find(el => (el.offsetTop + el.offsetHeight) > (container.scrollTop + 1))
884
- || pages[0];
885
- const page = parseInt(pageEl.getAttribute('data-page') || '1', 10);
886
- const height = Math.max(1, pageEl.offsetHeight);
887
- const ratio = Math.max(0, Math.min(1, (centerY - pageEl.offsetTop) / height));
888
- return { page, ratio };
889
- }
890
- // Fallback: single-canvas mode
891
- const canvas = container.querySelector('canvas');
892
- if (!canvas)
893
- return null;
894
- const height = Math.max(1, canvas.offsetHeight);
895
- const ratio = Math.max(0, Math.min(1, (centerY - canvas.offsetTop) / height));
896
- return { page: 1, ratio };
897
- }
898
- catch {
899
- return null;
900
- }
901
- }
902
- restoreScrollAnchor(container, anchor) {
903
- try {
904
- let el = container.querySelector(`[data-page="${anchor.page}"]`);
905
- if (!el)
906
- el = container.querySelector('canvas');
907
- if (!el)
908
- return;
909
- const prevBehavior = container.style.scrollBehavior;
910
- container.style.scrollBehavior = 'auto';
911
- const height = Math.max(1, el.offsetHeight);
912
- const desired = el.offsetTop + anchor.ratio * height - container.clientHeight / 2;
913
- const maxScroll = Math.max(0, container.scrollHeight - container.clientHeight);
914
- container.scrollTop = Math.max(0, Math.min(maxScroll, desired));
915
- container.style.scrollBehavior = prevBehavior;
916
- }
917
- catch { }
918
- }
919
- async download() {
920
- const filename = this.deriveFilename();
921
- const link = document.createElement('a');
922
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
923
- let popup = null;
924
- if (isIOS) {
925
- // Open a tab synchronously to stay within user gesture
926
- try {
927
- popup = window.open('', '_blank', 'noopener,noreferrer');
928
- }
929
- catch {
930
- popup = null;
931
- }
932
- }
933
- const blob = await this.createBlobFromSrc();
934
- if (blob) {
935
- const url = URL.createObjectURL(blob);
936
- if (isIOS) {
937
- if (popup) {
938
- try {
939
- popup.location.href = url;
940
- }
941
- catch { /* ignore */ }
942
- // Revoke later to allow the view to load fully
943
- setTimeout(() => URL.revokeObjectURL(url), 30000);
944
- }
945
- else {
946
- const a = document.createElement('a');
947
- a.href = url;
948
- a.target = '_blank';
949
- a.rel = 'noopener';
950
- document.body.appendChild(a);
951
- a.click();
952
- a.remove();
953
- setTimeout(() => URL.revokeObjectURL(url), 30000);
954
- }
955
- }
956
- else {
957
- link.href = url;
958
- link.download = filename;
959
- link.rel = 'noopener';
960
- link.style.display = 'none';
961
- document.body.appendChild(link);
962
- link.click();
963
- link.remove();
964
- URL.revokeObjectURL(url);
965
- }
966
- return;
967
- }
968
- // Fallback: cross-origin without CORS
969
- const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
970
- if (!href)
971
- return;
972
- if (isIOS) {
973
- if (popup) {
974
- try {
975
- popup.location.href = href;
976
- }
977
- catch { /* ignore */ }
978
- }
979
- else {
980
- const a = document.createElement('a');
981
- a.href = href;
982
- a.target = '_blank';
983
- a.rel = 'noopener';
984
- document.body.appendChild(a);
985
- a.click();
986
- a.remove();
987
- }
988
- }
989
- else {
990
- link.href = href;
991
- link.target = '_blank';
992
- link.rel = 'noopener';
993
- link.download = filename; // browsers may ignore for cross-origin
994
- link.style.display = 'none';
995
- document.body.appendChild(link);
996
- link.click();
997
- link.remove();
998
- }
999
- }
1000
- async print() {
1001
- // Robust, plugin-free printing by rendering pages to images
1002
- const ok = await this.printViaImages();
1003
- if (ok)
1004
- return;
1005
- // Fallbacks
1006
- const blob = await this.createBlobFromSrc();
1007
- if (blob) {
1008
- const url = URL.createObjectURL(blob);
1009
- const win = window.open('', '_blank', 'noopener,noreferrer');
1010
- if (win) {
1011
- win.document.write('<!doctype html><html><head><title>Print</title><meta charset="utf-8"><style>html,body{height:100%;margin:0}@page{size:auto;margin:0} iframe{border:0;width:100%;height:100%}</style></head><body><iframe src="' + url + '"></iframe></body></html>');
1012
- win.document.close();
1013
- setTimeout(() => { try {
1014
- win.focus();
1015
- win.print();
1016
- }
1017
- catch { } }, 800);
1018
- win.onafterprint = () => { try {
1019
- win.close();
1020
- }
1021
- catch { } URL.revokeObjectURL(url); };
1022
- }
1023
- else {
1024
- const iframe = document.createElement('iframe');
1025
- iframe.style.position = 'fixed';
1026
- iframe.style.right = '0';
1027
- iframe.style.bottom = '0';
1028
- iframe.style.width = '0';
1029
- iframe.style.height = '0';
1030
- iframe.style.border = '0';
1031
- iframe.src = url;
1032
- document.body.appendChild(iframe);
1033
- iframe.onload = () => { try {
1034
- setTimeout(() => iframe.contentWindow?.print(), 800);
1035
- }
1036
- catch { } setTimeout(() => { URL.revokeObjectURL(url); iframe.remove(); }, 3000); };
1037
- }
1038
- return;
1039
- }
1040
- const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
1041
- if (href)
1042
- window.open(href, '_blank');
1043
- }
1044
- async printViaImages() {
1045
- try {
1046
- let doc = this.doc;
1047
- if (!doc) {
1048
- doc = await this.loadDocumentWithFallbacks(this.src);
1049
- }
1050
- if (!doc)
1051
- return false;
1052
- const win = window.open('', '_blank', 'noopener,noreferrer');
1053
- if (!win)
1054
- return false;
1055
- win.document.write('<!doctype html><html><head><meta charset="utf-8"><title>Print</title><style>@page{size:auto;margin:0} html,body{margin:0;padding:0} .page{page-break-after:always;}</style></head><body></body></html>');
1056
- win.document.close();
1057
- for (let i = 1; i <= doc.numPages; i++) {
1058
- const page = await doc.getPage(i);
1059
- const vp = page.getViewport({ scale: this.PRINT_SCALE });
1060
- const canvas = document.createElement('canvas');
1061
- canvas.width = Math.floor(vp.width);
1062
- canvas.height = Math.floor(vp.height);
1063
- const ctx = canvas.getContext('2d');
1064
- await page.render({ canvas, canvasContext: ctx, viewport: vp }).promise;
1065
- const img = win.document.createElement('img');
1066
- img.className = 'page';
1067
- img.style.width = '100%';
1068
- img.style.display = 'block';
1069
- img.src = canvas.toDataURL('image/png');
1070
- win.document.body.appendChild(img);
1071
- }
1072
- setTimeout(() => { try {
1073
- win.focus();
1074
- win.print();
1075
- }
1076
- catch { } }, 300);
1077
- win.onafterprint = () => { try {
1078
- win.close();
1079
- }
1080
- catch { } };
1081
- setTimeout(() => { try {
1082
- win.close();
1083
- }
1084
- catch { } }, 60000);
1085
- return true;
1086
- }
1087
- catch {
1088
- return false;
1089
- }
1090
- }
1091
- async createBlobFromSrc() {
1092
- if (!this.src)
1093
- return null;
1094
- if (this.src instanceof Blob)
1095
- return this.src;
1096
- if (this.src instanceof Uint8Array) {
1097
- const ab = new ArrayBuffer(this.src.byteLength);
1098
- new Uint8Array(ab).set(this.src);
1099
- return new Blob([ab], { type: 'application/pdf' });
1100
- }
1101
- const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
1102
- if (!href || typeof fetch === 'undefined')
1103
- return null;
1104
- try {
1105
- const res = await fetch(href, {
1106
- mode: 'cors',
1107
- headers: this.httpHeaders,
1108
- credentials: this.withCredentials ? 'include' : 'same-origin',
1109
- });
1110
- if (!res.ok)
1111
- return null;
1112
- const blob = await res.blob();
1113
- return blob.type ? blob : new Blob([await res.arrayBuffer()], { type: 'application/pdf' });
1114
- }
1115
- catch {
1116
- return null;
1117
- }
1118
- }
1119
- deriveFilename() {
1120
- if (typeof this.src === 'string') {
1121
- try {
1122
- const u = new URL(this.src, window.location.origin);
1123
- return this.basename(u.pathname) || 'document.pdf';
1124
- }
1125
- catch {
1126
- return 'document.pdf';
1127
- }
1128
- }
1129
- if (this.src instanceof URL) {
1130
- return this.basename(this.src.pathname) || 'document.pdf';
1131
- }
1132
- return 'document.pdf';
1133
- }
1134
- basename(path) {
1135
- const last = path.split('/').filter(Boolean).pop() || '';
1136
- return last.endsWith('.pdf') ? last : (last ? last + '.pdf' : 'document.pdf');
1137
- }
1138
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: GenusPdfViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1139
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.1", type: GenusPdfViewerComponent, isStandalone: true, selector: "genus-pdf-viewer", inputs: { src: "src", page: "page", zoom: "zoom", maxZoom: "maxZoom", minZoom: "minZoom", fit: "fit", disableTextLayer: "disableTextLayer", continuous: "continuous", showToolbar: "showToolbar", zoomAnimation: "zoomAnimation", zoomAnimationMs: "zoomAnimationMs", gestureAnimationMs: "gestureAnimationMs", loadTimeoutMs: "loadTimeoutMs", allowIframeFallback: "allowIframeFallback", withCredentials: "withCredentials", httpHeaders: "httpHeaders" }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true }, { propertyName: "singleStage", first: true, predicate: ["singleStage"], descendants: true }, { propertyName: "stage", first: true, predicate: ["stage"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1140
- <div class="genus-pdf-shell">
1141
- <ng-content select="[toolbar]"></ng-content>
1142
-
1143
- <div class="genus-pdf-toolbar" *ngIf="showToolbar">
1144
- <button type="button" title="Zoom out" aria-label="Zoom out" (click)="zoomOut()">−</button>
1145
- <span class="pct">{{ (zoomSig() * 100) | number:'1.0-0' }}%</span>
1146
- <button type="button" title="Zoom in" aria-label="Zoom in" (click)="zoomIn()">+</button>
1147
- <button type="button" *ngIf="isZoomed()" title="Reset zoom to 100%" aria-label="Reset zoom to 100%" (click)="resetZoom()">100%</button>
1148
- <span class="spacer"></span>
1149
- <button type="button" title="Previous page" aria-label="Previous page" (click)="prevPage()" [disabled]="pageSig() <= 1">⟨</button>
1150
- <span>{{ pageSig() }} / {{ pageCount() }}</span>
1151
- <button type="button" title="Next page" aria-label="Next page" (click)="nextPage()" [disabled]="doc ? pageSig() >= doc.numPages : true">⟩</button>
1152
- <span class="spacer"></span>
1153
- <button type="button" class="download-btn" title="Download" aria-label="Download" (click)="download()">
1154
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1155
- <path d="M12 3v10m0 0 4-4m-4 4-4-4M5 15v3a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3v-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1156
- </svg>
1157
- <span>Download</span>
1158
- </button>
1159
- </div>
1160
-
1161
- <div class="genus-pdf-stage" #singleStage [style.display]="continuous ? 'none' : 'flex'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, singleStage)" (wheel)="onWheel($event, singleStage)" (scroll)="onScroll($event)">
1162
- <iframe *ngIf="iframeFallbackSrc()" class="genus-pdf-iframe" [src]="iframeFallbackSrc()" title="PDF document"></iframe>
1163
- <div class="genus-pdf-overlay" *ngIf="loading() && !iframeFallbackSrc()"><div class="spinner" aria-label="Loading"></div></div>
1164
- <div class="genus-pdf-overlay error" *ngIf="error() && !iframeFallbackSrc()">
1165
- <div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
1166
- <span>{{ error() }}</span>
1167
- <button type="button" (click)="retryLoad()">Retry</button>
1168
- </div>
1169
- </div>
1170
- <canvas #canvas [style.display]="iframeFallbackSrc() ? 'none' : 'block'"></canvas>
1171
- </div>
1172
-
1173
- <div class="genus-pdf-stage genus-pdf-stage--continuous" #stage [style.display]="continuous ? 'block' : 'none'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, stage)" (wheel)="onWheel($event, stage)" (scroll)="onScroll($event)">
1174
- <iframe *ngIf="iframeFallbackSrc()" class="genus-pdf-iframe" [src]="iframeFallbackSrc()" title="PDF document"></iframe>
1175
- <div class="genus-pdf-overlay" *ngIf="loading() && !iframeFallbackSrc()"><div class="spinner" aria-label="Loading"></div></div>
1176
- <div class="genus-pdf-overlay error" *ngIf="error() && !iframeFallbackSrc()">
1177
- <div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
1178
- <span>{{ error() }}</span>
1179
- <button type="button" (click)="retryLoad()">Retry</button>
1180
- </div>
1181
- </div>
1182
- </div>
1183
- </div>
1184
- `, isInline: true, styles: [".genus-pdf-shell{display:flex;flex-direction:column;gap:8px;width:100%;height:100%;background:#f3f4f6}.genus-pdf-toolbar{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid #e5e7eb;border-radius:8px;background:#ffffffd9;-webkit-backdrop-filter:saturate(1.2) blur(6px);backdrop-filter:saturate(1.2) blur(6px);box-shadow:0 1px 2px #0000000f}.genus-pdf-toolbar .pct{min-width:48px;text-align:center;font-variant-numeric:tabular-nums;color:#111827}.genus-pdf-toolbar button{appearance:none;border:1px solid #e5e7eb;background:#fff;padding:6px 10px;border-radius:6px;cursor:pointer;color:#111827;font-size:14px;line-height:1;font-weight:500;min-width:32px;-webkit-user-select:none;user-select:none;display:inline-flex;align-items:center;gap:6px}.genus-pdf-toolbar button.download-btn{background:#2563eb;border-color:#2563eb;color:#fff}.genus-pdf-toolbar button.download-btn:hover{background:#1d4ed8;border-color:#1d4ed8}.genus-pdf-toolbar button:disabled{opacity:.5;cursor:not-allowed}.genus-pdf-toolbar .spacer{flex:1}.genus-pdf-stage{position:relative;flex:1;overflow:auto;display:flex;align-items:flex-start;justify-content:center;background:#fafafa;touch-action:pan-y;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;overscroll-behavior-y:contain}.genus-pdf-stage.can-pan{cursor:grab;touch-action:none}.genus-pdf-stage.is-panning{cursor:grabbing}.genus-pdf-stage--continuous{display:block;align-items:unset;justify-content:unset;padding:8px;scroll-behavior:smooth}.genus-pdf-stage--continuous .pdf-page{margin:0 auto 12px;background:#fff;box-shadow:0 1px 2px #00000014}canvas{display:block;max-width:100%;height:auto}.genus-pdf-iframe{width:100%;height:100%;border:0;background:#fff}.genus-pdf-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffffb3;z-index:1}.genus-pdf-overlay.error{background:transparent}.spinner{width:28px;height:28px;border-radius:50%;border:3px solid #e5e7eb;border-top-color:#111827;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }] });
1185
- }
1186
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: GenusPdfViewerComponent, decorators: [{
1187
- type: Component,
1188
- args: [{ selector: 'genus-pdf-viewer', standalone: true, imports: [CommonModule], template: `
1189
- <div class="genus-pdf-shell">
1190
- <ng-content select="[toolbar]"></ng-content>
1191
-
1192
- <div class="genus-pdf-toolbar" *ngIf="showToolbar">
1193
- <button type="button" title="Zoom out" aria-label="Zoom out" (click)="zoomOut()">−</button>
1194
- <span class="pct">{{ (zoomSig() * 100) | number:'1.0-0' }}%</span>
1195
- <button type="button" title="Zoom in" aria-label="Zoom in" (click)="zoomIn()">+</button>
1196
- <button type="button" *ngIf="isZoomed()" title="Reset zoom to 100%" aria-label="Reset zoom to 100%" (click)="resetZoom()">100%</button>
1197
- <span class="spacer"></span>
1198
- <button type="button" title="Previous page" aria-label="Previous page" (click)="prevPage()" [disabled]="pageSig() <= 1">⟨</button>
1199
- <span>{{ pageSig() }} / {{ pageCount() }}</span>
1200
- <button type="button" title="Next page" aria-label="Next page" (click)="nextPage()" [disabled]="doc ? pageSig() >= doc.numPages : true">⟩</button>
1201
- <span class="spacer"></span>
1202
- <button type="button" class="download-btn" title="Download" aria-label="Download" (click)="download()">
1203
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1204
- <path d="M12 3v10m0 0 4-4m-4 4-4-4M5 15v3a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3v-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1205
- </svg>
1206
- <span>Download</span>
1207
- </button>
1208
- </div>
1209
-
1210
- <div class="genus-pdf-stage" #singleStage [style.display]="continuous ? 'none' : 'flex'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, singleStage)" (wheel)="onWheel($event, singleStage)" (scroll)="onScroll($event)">
1211
- <iframe *ngIf="iframeFallbackSrc()" class="genus-pdf-iframe" [src]="iframeFallbackSrc()" title="PDF document"></iframe>
1212
- <div class="genus-pdf-overlay" *ngIf="loading() && !iframeFallbackSrc()"><div class="spinner" aria-label="Loading"></div></div>
1213
- <div class="genus-pdf-overlay error" *ngIf="error() && !iframeFallbackSrc()">
1214
- <div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
1215
- <span>{{ error() }}</span>
1216
- <button type="button" (click)="retryLoad()">Retry</button>
1217
- </div>
1218
- </div>
1219
- <canvas #canvas [style.display]="iframeFallbackSrc() ? 'none' : 'block'"></canvas>
1220
- </div>
1221
-
1222
- <div class="genus-pdf-stage genus-pdf-stage--continuous" #stage [style.display]="continuous ? 'block' : 'none'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, stage)" (wheel)="onWheel($event, stage)" (scroll)="onScroll($event)">
1223
- <iframe *ngIf="iframeFallbackSrc()" class="genus-pdf-iframe" [src]="iframeFallbackSrc()" title="PDF document"></iframe>
1224
- <div class="genus-pdf-overlay" *ngIf="loading() && !iframeFallbackSrc()"><div class="spinner" aria-label="Loading"></div></div>
1225
- <div class="genus-pdf-overlay error" *ngIf="error() && !iframeFallbackSrc()">
1226
- <div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
1227
- <span>{{ error() }}</span>
1228
- <button type="button" (click)="retryLoad()">Retry</button>
1229
- </div>
1230
- </div>
1231
- </div>
1232
- </div>
1233
- `, styles: [".genus-pdf-shell{display:flex;flex-direction:column;gap:8px;width:100%;height:100%;background:#f3f4f6}.genus-pdf-toolbar{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid #e5e7eb;border-radius:8px;background:#ffffffd9;-webkit-backdrop-filter:saturate(1.2) blur(6px);backdrop-filter:saturate(1.2) blur(6px);box-shadow:0 1px 2px #0000000f}.genus-pdf-toolbar .pct{min-width:48px;text-align:center;font-variant-numeric:tabular-nums;color:#111827}.genus-pdf-toolbar button{appearance:none;border:1px solid #e5e7eb;background:#fff;padding:6px 10px;border-radius:6px;cursor:pointer;color:#111827;font-size:14px;line-height:1;font-weight:500;min-width:32px;-webkit-user-select:none;user-select:none;display:inline-flex;align-items:center;gap:6px}.genus-pdf-toolbar button.download-btn{background:#2563eb;border-color:#2563eb;color:#fff}.genus-pdf-toolbar button.download-btn:hover{background:#1d4ed8;border-color:#1d4ed8}.genus-pdf-toolbar button:disabled{opacity:.5;cursor:not-allowed}.genus-pdf-toolbar .spacer{flex:1}.genus-pdf-stage{position:relative;flex:1;overflow:auto;display:flex;align-items:flex-start;justify-content:center;background:#fafafa;touch-action:pan-y;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;overscroll-behavior-y:contain}.genus-pdf-stage.can-pan{cursor:grab;touch-action:none}.genus-pdf-stage.is-panning{cursor:grabbing}.genus-pdf-stage--continuous{display:block;align-items:unset;justify-content:unset;padding:8px;scroll-behavior:smooth}.genus-pdf-stage--continuous .pdf-page{margin:0 auto 12px;background:#fff;box-shadow:0 1px 2px #00000014}canvas{display:block;max-width:100%;height:auto}.genus-pdf-iframe{width:100%;height:100%;border:0;background:#fff}.genus-pdf-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffffb3;z-index:1}.genus-pdf-overlay.error{background:transparent}.spinner{width:28px;height:28px;border-radius:50%;border:3px solid #e5e7eb;border-top-color:#111827;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
1234
- }], ctorParameters: () => [], propDecorators: { src: [{
1235
- type: Input
1236
- }], page: [{
1237
- type: Input
1238
- }], zoom: [{
1239
- type: Input
1240
- }], maxZoom: [{
1241
- type: Input
1242
- }], minZoom: [{
1243
- type: Input
1244
- }], fit: [{
1245
- type: Input
1246
- }], disableTextLayer: [{
1247
- type: Input
1248
- }], continuous: [{
1249
- type: Input
1250
- }], showToolbar: [{
1251
- type: Input
1252
- }], zoomAnimation: [{
1253
- type: Input
1254
- }], zoomAnimationMs: [{
1255
- type: Input
1256
- }], gestureAnimationMs: [{
1257
- type: Input
1258
- }], loadTimeoutMs: [{
1259
- type: Input
1260
- }], allowIframeFallback: [{
1261
- type: Input
1262
- }], withCredentials: [{
1263
- type: Input
1264
- }], httpHeaders: [{
1265
- type: Input
1266
- }], canvas: [{
1267
- type: ViewChild,
1268
- args: ['canvas', { static: false }]
1269
- }], singleStage: [{
1270
- type: ViewChild,
1271
- args: ['singleStage', { static: false }]
1272
- }], stage: [{
1273
- type: ViewChild,
1274
- args: ['stage', { static: false }]
1275
- }] } });
1276
-
1277
- const GENUS_PDF_WORKER_CONFIG = new InjectionToken('GENUS_PDF_WORKER_CONFIG');
1278
- function resolveBundledWorkerUrl() {
1279
- try {
1280
- return new URL('../workers/pdf.worker.min.mjs', import.meta.url).toString();
1281
- }
1282
- catch {
1283
- return null;
1284
- }
1285
- }
1286
- async function setupInlineWorkerFallback() {
1287
- const g = globalThis;
1288
- if (g?.pdfjsWorker?.WorkerMessageHandler)
1289
- return;
1290
- try {
1291
- // @ts-ignore pdfjs-dist worker entry does not ship declaration files.
1292
- const workerModule = await import('pdfjs-dist/build/pdf.worker.min.mjs');
1293
- if (workerModule?.WorkerMessageHandler) {
1294
- g.pdfjsWorker = { ...(g.pdfjsWorker || {}), WorkerMessageHandler: workerModule.WorkerMessageHandler };
1295
- }
1296
- }
1297
- catch { }
1298
- }
1299
- function initPdfWorkerFactory(cfg, platformId) {
1300
- return async () => {
1301
- if (!isPlatformBrowser(platformId))
1302
- return;
1303
- const defaults = {
1304
- tryModuleWorker: true,
1305
- inlineWorkerFallback: true,
1306
- };
1307
- const merged = { ...defaults, ...cfg };
1308
- const globalOptions = pdfjsLib.GlobalWorkerOptions;
1309
- globalOptions.workerPort = null;
1310
- if (merged.inlineWorkerFallback) {
1311
- await setupInlineWorkerFallback();
1312
- }
1313
- if (typeof merged.workerSrc === 'string' && merged.workerSrc.length > 0) {
1314
- globalOptions.workerSrc = merged.workerSrc;
1315
- return;
1316
- }
1317
- const bundledWorkerUrl = resolveBundledWorkerUrl();
1318
- if (!merged.tryModuleWorker) {
1319
- globalOptions.workerSrc = bundledWorkerUrl || '/assets/pdf.worker.min.js';
1320
- return;
1321
- }
1322
- globalOptions.workerSrc = bundledWorkerUrl || '/assets/pdf.worker.min.js';
1323
- };
1324
- }
1325
- function provideGenusPdfViewer(cfg = {}) {
1326
- return [
1327
- { provide: GENUS_PDF_WORKER_CONFIG, useValue: cfg },
1328
- {
1329
- provide: APP_INITIALIZER,
1330
- multi: true,
1331
- useFactory: initPdfWorkerFactory,
1332
- deps: [GENUS_PDF_WORKER_CONFIG, PLATFORM_ID],
1333
- },
1334
- ];
1335
- }
1336
-
1337
- /*
1338
- * Public API Surface of genus-pdf-viewer
1339
- */
1340
-
1341
- /**
1342
- * Generated bundle index. Do not edit.
1343
- */
1344
-
1345
- export { GENUS_PDF_WORKER_CONFIG, GenusPdfViewerComponent, provideGenusPdfViewer };
1346
- //# sourceMappingURL=genus-pdf-viewer.mjs.map