genus-pdf-viewer 0.1.7 → 0.1.9
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/fesm2022/genus-pdf-viewer.mjs +565 -194
- package/fesm2022/genus-pdf-viewer.mjs.map +1 -1
- package/index.d.ts +35 -12
- package/package.json +1 -1
|
@@ -14,6 +14,9 @@ class GenusPdfViewerComponent {
|
|
|
14
14
|
disableTextLayer = true;
|
|
15
15
|
continuous = true;
|
|
16
16
|
showToolbar = true;
|
|
17
|
+
zoomAnimation = true;
|
|
18
|
+
zoomAnimationMs = 180;
|
|
19
|
+
gestureAnimationMs = 120;
|
|
17
20
|
canvas;
|
|
18
21
|
singleStage;
|
|
19
22
|
stage;
|
|
@@ -27,46 +30,64 @@ class GenusPdfViewerComponent {
|
|
|
27
30
|
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
28
31
|
PRINT_SCALE = 2;
|
|
29
32
|
scrollRaf = 0;
|
|
30
|
-
pinchRaf = 0;
|
|
31
|
-
pinchActive = false;
|
|
32
|
-
pinchStartDistance = 0;
|
|
33
|
-
pinchStartZoom = 1;
|
|
34
|
-
pinchAnchorClient = null;
|
|
35
|
-
activePointers = new Map();
|
|
36
33
|
renderLockCount = 0;
|
|
34
|
+
zoomAnimInFlight = false;
|
|
35
|
+
activeRenderTasks = new Set();
|
|
36
|
+
wheelRaf = 0;
|
|
37
|
+
pinchState = null;
|
|
38
|
+
activePointers = new Map();
|
|
39
|
+
renderVersion = 0;
|
|
40
|
+
previewActive = false;
|
|
41
|
+
reRenderQueued = false;
|
|
42
|
+
pendingAnchor;
|
|
43
|
+
lastZoomClient = null;
|
|
44
|
+
WHEEL_STEP = 0.02;
|
|
45
|
+
PINCH_SENSITIVITY = 0.5;
|
|
46
|
+
wheelCommitTimer = 0;
|
|
37
47
|
constructor() {
|
|
38
48
|
effect(() => {
|
|
39
|
-
//
|
|
40
|
-
this.pageSig();
|
|
49
|
+
// Only react to zoom changes here; page changes in continuous mode should NOT trigger a re-render
|
|
41
50
|
this.zoomSig();
|
|
42
|
-
if (this.renderLockCount > 0)
|
|
51
|
+
if (this.renderLockCount > 0 || this.previewActive)
|
|
43
52
|
return;
|
|
44
53
|
this.safeRender();
|
|
45
54
|
});
|
|
46
55
|
}
|
|
47
|
-
async ngOnChanges() {
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
async ngOnChanges(changes) {
|
|
57
|
+
if (changes['page'])
|
|
58
|
+
this.pageSig.set(this.page);
|
|
59
|
+
if (changes['zoom'])
|
|
60
|
+
this.zoomSig.set(this.zoom);
|
|
61
|
+
const srcChanged = !!changes['src'];
|
|
62
|
+
const layoutChanged = !!(changes['fit'] || changes['minZoom'] || changes['maxZoom']);
|
|
50
63
|
if (!this.src)
|
|
51
64
|
return;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
if (srcChanged) {
|
|
66
|
+
await this.loadDocument();
|
|
67
|
+
if (this.doc) {
|
|
68
|
+
const pages = this.doc.numPages;
|
|
69
|
+
if (this.pageSig() < 1)
|
|
70
|
+
this.pageSig.set(1);
|
|
71
|
+
if (this.pageSig() > pages)
|
|
72
|
+
this.pageSig.set(pages);
|
|
73
|
+
await this.render();
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
57
76
|
}
|
|
58
|
-
|
|
59
|
-
this.
|
|
60
|
-
|
|
77
|
+
if (layoutChanged) {
|
|
78
|
+
if (this.continuous) {
|
|
79
|
+
const container = this.stage?.nativeElement;
|
|
80
|
+
const anchor = container ? this.captureScrollAnchor(container) || undefined : undefined;
|
|
81
|
+
await this.renderAllPages(anchor);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await this.render();
|
|
85
|
+
}
|
|
61
86
|
return;
|
|
62
87
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (this.pageSig() > pages)
|
|
67
|
-
this.pageSig.set(pages);
|
|
68
|
-
await this.render();
|
|
69
|
-
this.loading.set(false);
|
|
88
|
+
if (!this.continuous && changes['page']) {
|
|
89
|
+
await this.render();
|
|
90
|
+
}
|
|
70
91
|
}
|
|
71
92
|
async safeRender() {
|
|
72
93
|
try {
|
|
@@ -74,9 +95,48 @@ class GenusPdfViewerComponent {
|
|
|
74
95
|
}
|
|
75
96
|
catch (_) { }
|
|
76
97
|
}
|
|
98
|
+
async loadDocument() {
|
|
99
|
+
this.loading.set(true);
|
|
100
|
+
this.error.set(null);
|
|
101
|
+
try {
|
|
102
|
+
const task = pdfjsLib.getDocument(this.normalizePdfSource(this.src));
|
|
103
|
+
this.doc = await task.promise;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
// Fallback for environments (notably some iOS builds) where range/streaming fetch fails.
|
|
107
|
+
// Try fetching the whole file via fetch() and pass ArrayBuffer to pdf.js.
|
|
108
|
+
try {
|
|
109
|
+
const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
|
|
110
|
+
if (href && typeof fetch !== 'undefined') {
|
|
111
|
+
const res = await fetch(href, { mode: 'cors' });
|
|
112
|
+
if (res.ok) {
|
|
113
|
+
const ab = await res.arrayBuffer();
|
|
114
|
+
const data = new Uint8Array(ab);
|
|
115
|
+
const task2 = pdfjsLib.getDocument({ data });
|
|
116
|
+
this.doc = await task2.promise;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.doc = undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.doc = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
this.doc = undefined;
|
|
128
|
+
}
|
|
129
|
+
if (!this.doc)
|
|
130
|
+
this.error.set('Failed to load PDF');
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.loading.set(false);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
77
136
|
async render() {
|
|
78
137
|
if (!this.doc)
|
|
79
138
|
return;
|
|
139
|
+
const version = ++this.renderVersion;
|
|
80
140
|
if (this.continuous) {
|
|
81
141
|
await this.renderAllPages();
|
|
82
142
|
return;
|
|
@@ -90,6 +150,7 @@ class GenusPdfViewerComponent {
|
|
|
90
150
|
const ctx = canvas.getContext('2d');
|
|
91
151
|
const baseViewport = pdfPage.getViewport({ scale: 1 });
|
|
92
152
|
const parent = canvas.parentElement;
|
|
153
|
+
const anchor = this.captureScrollAnchor(parent) || undefined;
|
|
93
154
|
const pW = parent.clientWidth, pH = parent.clientHeight || baseViewport.height;
|
|
94
155
|
let baseScale = 1;
|
|
95
156
|
if (this.fit === 'width')
|
|
@@ -111,7 +172,12 @@ class GenusPdfViewerComponent {
|
|
|
111
172
|
const renderCtx = { canvasContext: ctx, viewport };
|
|
112
173
|
if (dpr !== 1)
|
|
113
174
|
renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
|
|
114
|
-
|
|
175
|
+
const task = pdfPage.render(renderCtx);
|
|
176
|
+
await task.promise;
|
|
177
|
+
if (this.renderVersion !== version)
|
|
178
|
+
return;
|
|
179
|
+
if (anchor)
|
|
180
|
+
this.restoreScrollAnchor(parent, anchor);
|
|
115
181
|
}
|
|
116
182
|
async renderAllPages(anchorOverride) {
|
|
117
183
|
if (!this.doc)
|
|
@@ -119,63 +185,104 @@ class GenusPdfViewerComponent {
|
|
|
119
185
|
const stageEl = this.stage?.nativeElement;
|
|
120
186
|
if (!stageEl)
|
|
121
187
|
return;
|
|
188
|
+
const version = ++this.renderVersion;
|
|
122
189
|
const anchor = anchorOverride ?? this.captureScrollAnchor(stageEl);
|
|
123
|
-
const placeholderMinHeight = stageEl.scrollHeight;
|
|
124
|
-
if (placeholderMinHeight > 0)
|
|
125
|
-
stageEl.style.minHeight = placeholderMinHeight + 'px';
|
|
126
|
-
const fragment = document.createDocumentFragment();
|
|
127
190
|
const total = this.doc.numPages;
|
|
128
191
|
const parentRect = stageEl.getBoundingClientRect();
|
|
129
192
|
const parentWidth = parentRect.width || stageEl.clientWidth || 800;
|
|
130
193
|
let computedCanPan = false;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
194
|
+
const existing = Array.from(stageEl.querySelectorAll('.pdf-page'));
|
|
195
|
+
const isInitialBuild = existing.length !== total;
|
|
196
|
+
this.renderLockCount++;
|
|
197
|
+
if (isInitialBuild) {
|
|
198
|
+
stageEl.innerHTML = '';
|
|
199
|
+
const contentEl = document.createElement('div');
|
|
200
|
+
contentEl.className = 'pdf-content';
|
|
201
|
+
stageEl.appendChild(contentEl);
|
|
202
|
+
for (let i = 1; i <= total; i++) {
|
|
203
|
+
const page = await this.doc.getPage(i);
|
|
204
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
205
|
+
let baseScale = 1;
|
|
206
|
+
if (this.fit === 'width')
|
|
207
|
+
baseScale = (parentWidth - 16) / baseViewport.width;
|
|
208
|
+
if (this.fit === 'page')
|
|
209
|
+
baseScale = (parentWidth - 16) / baseViewport.width;
|
|
210
|
+
const minScale = baseScale * this.minZoom;
|
|
211
|
+
const maxScale = baseScale * this.maxZoom;
|
|
212
|
+
const finalScale = Math.max(minScale, Math.min(maxScale, baseScale * this.zoomSig()));
|
|
213
|
+
if (i === 1)
|
|
214
|
+
computedCanPan = finalScale > baseScale + 0.001;
|
|
215
|
+
const viewport = page.getViewport({ scale: finalScale });
|
|
216
|
+
const wrapper = document.createElement('div');
|
|
217
|
+
wrapper.className = 'pdf-page';
|
|
218
|
+
wrapper.setAttribute('data-page', String(i));
|
|
219
|
+
wrapper.style.width = Math.floor(viewport.width) + 'px';
|
|
220
|
+
wrapper.style.height = Math.floor(viewport.height) + 'px';
|
|
221
|
+
wrapper.style.margin = '0 auto 12px auto';
|
|
222
|
+
const canvas = document.createElement('canvas');
|
|
223
|
+
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
|
|
224
|
+
canvas.style.width = Math.floor(viewport.width) + 'px';
|
|
225
|
+
canvas.style.height = Math.floor(viewport.height) + 'px';
|
|
226
|
+
canvas.width = Math.floor(viewport.width * dpr);
|
|
227
|
+
canvas.height = Math.floor(viewport.height * dpr);
|
|
228
|
+
const ctx = canvas.getContext('2d');
|
|
229
|
+
wrapper.appendChild(canvas);
|
|
230
|
+
contentEl.appendChild(wrapper);
|
|
231
|
+
const renderCtx = { canvasContext: ctx, viewport };
|
|
232
|
+
if (dpr !== 1)
|
|
233
|
+
renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
|
|
234
|
+
const task = page.render(renderCtx);
|
|
235
|
+
await task.promise;
|
|
236
|
+
if (this.renderVersion !== version)
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
let contentEl = stageEl.querySelector('.pdf-content');
|
|
242
|
+
if (!contentEl) {
|
|
243
|
+
contentEl = document.createElement('div');
|
|
244
|
+
contentEl.className = 'pdf-content';
|
|
245
|
+
const pages = Array.from(stageEl.querySelectorAll('.pdf-page'));
|
|
246
|
+
for (const el of pages)
|
|
247
|
+
contentEl.appendChild(el);
|
|
248
|
+
stageEl.appendChild(contentEl);
|
|
249
|
+
}
|
|
250
|
+
for (let i = 1; i <= total; i++) {
|
|
251
|
+
const page = await this.doc.getPage(i);
|
|
252
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
253
|
+
let baseScale = 1;
|
|
254
|
+
if (this.fit === 'width')
|
|
255
|
+
baseScale = (parentWidth - 16) / baseViewport.width;
|
|
256
|
+
if (this.fit === 'page')
|
|
257
|
+
baseScale = (parentWidth - 16) / baseViewport.width;
|
|
258
|
+
const minScale = baseScale * this.minZoom;
|
|
259
|
+
const maxScale = baseScale * this.maxZoom;
|
|
260
|
+
const finalScale = Math.max(minScale, Math.min(maxScale, baseScale * this.zoomSig()));
|
|
261
|
+
if (i === 1)
|
|
262
|
+
computedCanPan = finalScale > baseScale + 0.001;
|
|
263
|
+
const viewport = page.getViewport({ scale: finalScale });
|
|
264
|
+
const wrapper = existing[i - 1];
|
|
265
|
+
const canvas = wrapper.querySelector('canvas');
|
|
266
|
+
wrapper.style.width = Math.floor(viewport.width) + 'px';
|
|
267
|
+
wrapper.style.height = Math.floor(viewport.height) + 'px';
|
|
268
|
+
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
|
|
269
|
+
canvas.style.width = Math.floor(viewport.width) + 'px';
|
|
270
|
+
canvas.style.height = Math.floor(viewport.height) + 'px';
|
|
271
|
+
canvas.width = Math.floor(viewport.width * dpr);
|
|
272
|
+
canvas.height = Math.floor(viewport.height * dpr);
|
|
273
|
+
const ctx = canvas.getContext('2d');
|
|
274
|
+
const renderCtx = { canvasContext: ctx, viewport };
|
|
275
|
+
if (dpr !== 1)
|
|
276
|
+
renderCtx.transform = [dpr, 0, 0, dpr, 0, 0];
|
|
277
|
+
const task = page.render(renderCtx);
|
|
278
|
+
await task.promise;
|
|
279
|
+
if (this.renderVersion !== version)
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
163
282
|
}
|
|
164
283
|
this.canPan.set(computedCanPan);
|
|
165
|
-
const prevBehavior = stageEl.style.scrollBehavior;
|
|
166
|
-
const prevOverflow = stageEl.style.overflow;
|
|
167
|
-
const prevVisibility = stageEl.style.visibility;
|
|
168
|
-
this.renderLockCount++;
|
|
169
|
-
stageEl.style.scrollBehavior = 'auto';
|
|
170
|
-
stageEl.style.overflow = 'hidden';
|
|
171
|
-
stageEl.style.visibility = 'hidden';
|
|
172
|
-
stageEl.replaceChildren(fragment);
|
|
173
284
|
if (anchor)
|
|
174
285
|
this.restoreScrollAnchor(stageEl, anchor);
|
|
175
|
-
stageEl.style.scrollBehavior = prevBehavior;
|
|
176
|
-
stageEl.style.overflow = prevOverflow;
|
|
177
|
-
stageEl.style.visibility = prevVisibility;
|
|
178
|
-
stageEl.style.minHeight = '';
|
|
179
286
|
this.renderLockCount = Math.max(0, this.renderLockCount - 1);
|
|
180
287
|
}
|
|
181
288
|
async goTo(page) {
|
|
@@ -192,27 +299,108 @@ class GenusPdfViewerComponent {
|
|
|
192
299
|
await this.render();
|
|
193
300
|
}
|
|
194
301
|
}
|
|
195
|
-
async zoomIn(step = 0.1) {
|
|
196
|
-
|
|
302
|
+
async zoomIn(step = 0.1) { await this.applyZoomDelta(step); }
|
|
303
|
+
async zoomOut(step = 0.1) { await this.applyZoomDelta(-step); }
|
|
304
|
+
clampZoom(z) {
|
|
305
|
+
return Math.max(this.minZoom, Math.min(this.maxZoom, z));
|
|
306
|
+
}
|
|
307
|
+
cancelPreviewIfAny(container) {
|
|
308
|
+
if (!this.previewActive || !container)
|
|
309
|
+
return;
|
|
310
|
+
const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
|
|
311
|
+
try {
|
|
312
|
+
target.style.transform = '';
|
|
313
|
+
target.style.willChange = '';
|
|
314
|
+
target.style.transition = '';
|
|
315
|
+
}
|
|
316
|
+
catch { }
|
|
317
|
+
this.previewActive = false;
|
|
318
|
+
this.lastZoomClient = null;
|
|
319
|
+
}
|
|
320
|
+
async applyZoomDelta(delta) {
|
|
321
|
+
const oldZoom = this.zoomSig();
|
|
322
|
+
const newZoom = this.clampZoom(oldZoom + delta);
|
|
323
|
+
if (Math.abs(newZoom - oldZoom) < 0.0001)
|
|
324
|
+
return;
|
|
197
325
|
const container = this.continuous ? this.stage?.nativeElement : this.singleStage?.nativeElement;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
326
|
+
// If a preview transform is active (from pinch/wheel), clear it before capturing anchor or animating
|
|
327
|
+
this.cancelPreviewIfAny(container);
|
|
328
|
+
const anchor = container ? this.captureScrollAnchor(container) || undefined : undefined;
|
|
329
|
+
const ratio = newZoom / oldZoom;
|
|
330
|
+
if (this.zoomAnimation && container && !this.zoomAnimInFlight) {
|
|
331
|
+
this.zoomAnimInFlight = true;
|
|
332
|
+
this.renderLockCount++;
|
|
333
|
+
// Update UI value immediately
|
|
334
|
+
this.zoomSig.set(newZoom);
|
|
335
|
+
try {
|
|
336
|
+
await this.runZoomAnimation(container, ratio);
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
// Release the render lock before triggering the actual re-render
|
|
340
|
+
this.renderLockCount = Math.max(0, this.renderLockCount - 1);
|
|
341
|
+
await this.safeReRenderWithCancellation(anchor);
|
|
342
|
+
this.zoomAnimInFlight = false;
|
|
343
|
+
}
|
|
201
344
|
}
|
|
202
345
|
else {
|
|
203
|
-
|
|
346
|
+
this.zoomSig.set(newZoom);
|
|
347
|
+
await this.safeReRenderWithCancellation(anchor);
|
|
204
348
|
}
|
|
205
349
|
}
|
|
206
|
-
async
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
350
|
+
async runZoomAnimation(container, ratio) {
|
|
351
|
+
if (Math.abs(ratio - 1) < 0.0001)
|
|
352
|
+
return;
|
|
353
|
+
const { target, originX, originY } = this.getZoomAnimTarget(container);
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
try {
|
|
356
|
+
const onDone = () => {
|
|
357
|
+
cleanup();
|
|
358
|
+
resolve();
|
|
359
|
+
};
|
|
360
|
+
const cleanup = () => {
|
|
361
|
+
target.removeEventListener('transitionend', onEnd);
|
|
362
|
+
target.style.transition = '';
|
|
363
|
+
target.style.transform = '';
|
|
364
|
+
target.style.transformOrigin = '';
|
|
365
|
+
target.style.willChange = '';
|
|
366
|
+
};
|
|
367
|
+
const onEnd = () => onDone();
|
|
368
|
+
target.style.willChange = 'transform';
|
|
369
|
+
target.style.transformOrigin = `${Math.max(0, originX)}px ${Math.max(0, originY)}px`;
|
|
370
|
+
target.style.transition = 'none';
|
|
371
|
+
target.style.transform = 'scale(1)';
|
|
372
|
+
requestAnimationFrame(() => {
|
|
373
|
+
target.addEventListener('transitionend', onEnd, { once: true });
|
|
374
|
+
target.style.transition = `transform ${this.zoomAnimationMs}ms cubic-bezier(0.2,0,0,1)`;
|
|
375
|
+
target.style.transform = `scale(${ratio})`;
|
|
376
|
+
setTimeout(onDone, this.zoomAnimationMs + 60);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
resolve();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
getZoomAnimTarget(container) {
|
|
385
|
+
const isContinuous = this.continuous;
|
|
386
|
+
let target = null;
|
|
387
|
+
if (isContinuous) {
|
|
388
|
+
target = container.querySelector('.pdf-content');
|
|
389
|
+
if (!target)
|
|
390
|
+
target = container; // fallback
|
|
212
391
|
}
|
|
213
392
|
else {
|
|
214
|
-
|
|
393
|
+
target = container.querySelector('canvas');
|
|
394
|
+
if (!target)
|
|
395
|
+
target = container; // fallback
|
|
215
396
|
}
|
|
397
|
+
const rectLeft = target.offsetLeft;
|
|
398
|
+
const rectTop = target.offsetTop;
|
|
399
|
+
// Prefer last gesture point (pinch center or wheel pointer) if available
|
|
400
|
+
const client = this.lastZoomClient;
|
|
401
|
+
const originX = client ? (container.scrollLeft + client.x - rectLeft) : (container.scrollLeft + container.clientWidth / 2 - rectLeft);
|
|
402
|
+
const originY = client ? (container.scrollTop + client.y - rectTop) : (container.scrollTop + container.clientHeight / 2 - rectTop);
|
|
403
|
+
return { target, originX, originY };
|
|
216
404
|
}
|
|
217
405
|
onScroll(evt) {
|
|
218
406
|
const container = evt.target;
|
|
@@ -242,61 +430,80 @@ class GenusPdfViewerComponent {
|
|
|
242
430
|
}
|
|
243
431
|
async nextPage() { await this.goTo(this.pageSig() + 1); }
|
|
244
432
|
async prevPage() { await this.goTo(this.pageSig() - 1); }
|
|
433
|
+
isZoomed() {
|
|
434
|
+
return Math.abs(this.zoomSig() - 1) > 0.0001;
|
|
435
|
+
}
|
|
436
|
+
async resetZoom() {
|
|
437
|
+
// Reset to exactly 1.0 (100%)
|
|
438
|
+
const delta = 1 - this.zoomSig();
|
|
439
|
+
await this.applyZoomDelta(delta);
|
|
440
|
+
}
|
|
441
|
+
async retryLoad() {
|
|
442
|
+
await this.loadDocument();
|
|
443
|
+
if (this.doc) {
|
|
444
|
+
const pages = this.doc.numPages;
|
|
445
|
+
if (this.pageSig() < 1)
|
|
446
|
+
this.pageSig.set(1);
|
|
447
|
+
if (this.pageSig() > pages)
|
|
448
|
+
this.pageSig.set(pages);
|
|
449
|
+
await this.render();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
245
452
|
onPointerDown(evt, containerEl) {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
453
|
+
// Pinch-to-zoom (two pointers)
|
|
454
|
+
if (evt.pointerType === 'touch') {
|
|
455
|
+
this.activePointers.set(evt.pointerId, { x: evt.clientX, y: evt.clientY, type: evt.pointerType });
|
|
456
|
+
const onPointerMove = (e) => {
|
|
457
|
+
if (e.pointerType === 'touch')
|
|
458
|
+
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY, type: e.pointerType });
|
|
459
|
+
const touches = Array.from(this.activePointers.entries()).map(([id, p]) => ({ pointerId: id, clientX: p.x, clientY: p.y }))
|
|
460
|
+
.slice(0, 2);
|
|
461
|
+
if (touches.length === 2) {
|
|
462
|
+
const [a, b] = touches;
|
|
463
|
+
const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
464
|
+
if (!this.pinchState) {
|
|
465
|
+
this.pinchState = { id1: a.pointerId, id2: b.pointerId, startDist: Math.max(1, dist), startZoom: this.zoomSig() };
|
|
466
|
+
const rect = containerEl.getBoundingClientRect();
|
|
467
|
+
this.lastZoomClient = { x: ((a.clientX + b.clientX) / 2) - rect.left, y: ((a.clientY + b.clientY) / 2) - rect.top };
|
|
468
|
+
}
|
|
469
|
+
const scale = dist / Math.max(1, this.pinchState.startDist);
|
|
470
|
+
const adjusted = 1 + (scale - 1) * this.PINCH_SENSITIVITY;
|
|
471
|
+
const desiredAdj = this.clampZoom(this.pinchState.startZoom * adjusted);
|
|
472
|
+
this.previewZoom(containerEl, desiredAdj);
|
|
473
|
+
e.preventDefault();
|
|
262
474
|
return;
|
|
263
|
-
const p = this.activePointers.get(e.pointerId);
|
|
264
|
-
if (p) {
|
|
265
|
-
p.x = e.clientX;
|
|
266
|
-
p.y = e.clientY;
|
|
267
475
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!this.pinchRaf) {
|
|
275
|
-
this.pinchRaf = requestAnimationFrame(() => {
|
|
276
|
-
this.pinchRaf = 0;
|
|
277
|
-
this.zoomSig.set(targetZoom);
|
|
278
|
-
this.renderWithAnchor(containerEl, this.pinchAnchorClient);
|
|
279
|
-
});
|
|
476
|
+
// When pinch ends (not exactly two touches), commit render if we were previewing
|
|
477
|
+
if (this.pinchState) {
|
|
478
|
+
const final = this.zoomSig();
|
|
479
|
+
this.commitPreviewZoom(containerEl, final);
|
|
480
|
+
this.pinchState = null;
|
|
481
|
+
this.lastZoomClient = null;
|
|
280
482
|
}
|
|
281
|
-
e.preventDefault();
|
|
282
483
|
};
|
|
283
|
-
const
|
|
484
|
+
const onPointerEnd = (e) => {
|
|
284
485
|
this.activePointers.delete(e.pointerId);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
containerEl.style.touchAction = '';
|
|
486
|
+
const touches = Array.from(this.activePointers.values());
|
|
487
|
+
if (touches.length !== 2 && this.pinchState) {
|
|
488
|
+
const final = this.zoomSig();
|
|
489
|
+
this.commitPreviewZoom(containerEl, final);
|
|
490
|
+
this.pinchState = null;
|
|
491
|
+
this.lastZoomClient = null;
|
|
292
492
|
}
|
|
493
|
+
cleanup();
|
|
293
494
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
containerEl.removeEventListener('pointermove', onPointerMove);
|
|
497
|
+
containerEl.removeEventListener('pointerup', onPointerEnd);
|
|
498
|
+
containerEl.removeEventListener('pointercancel', onPointerEnd);
|
|
499
|
+
this.activePointers.clear();
|
|
500
|
+
};
|
|
501
|
+
containerEl.addEventListener('pointermove', onPointerMove);
|
|
502
|
+
containerEl.addEventListener('pointerup', onPointerEnd);
|
|
503
|
+
containerEl.addEventListener('pointercancel', onPointerEnd);
|
|
297
504
|
return;
|
|
298
505
|
}
|
|
299
|
-
//
|
|
506
|
+
// Mouse pan when canPan
|
|
300
507
|
if (!this.canPan())
|
|
301
508
|
return;
|
|
302
509
|
this.isPanning.set(true);
|
|
@@ -307,9 +514,6 @@ class GenusPdfViewerComponent {
|
|
|
307
514
|
containerEl.style.scrollBehavior = 'auto';
|
|
308
515
|
containerEl.setPointerCapture?.(evt.pointerId);
|
|
309
516
|
const move = (e) => {
|
|
310
|
-
if (this.pinchActive)
|
|
311
|
-
return; // ignore while pinching
|
|
312
|
-
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
313
517
|
const dx = e.clientX - startX;
|
|
314
518
|
const dy = e.clientY - startY;
|
|
315
519
|
containerEl.scrollLeft = startLeft - dx;
|
|
@@ -317,7 +521,6 @@ class GenusPdfViewerComponent {
|
|
|
317
521
|
e.preventDefault();
|
|
318
522
|
};
|
|
319
523
|
const end = (e) => {
|
|
320
|
-
this.activePointers.delete(e.pointerId);
|
|
321
524
|
this.isPanning.set(false);
|
|
322
525
|
containerEl.releasePointerCapture?.(evt.pointerId);
|
|
323
526
|
containerEl.removeEventListener('pointermove', move);
|
|
@@ -329,32 +532,109 @@ class GenusPdfViewerComponent {
|
|
|
329
532
|
containerEl.addEventListener('pointerup', end);
|
|
330
533
|
containerEl.addEventListener('pointercancel', end);
|
|
331
534
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
535
|
+
getActiveTouches(containerEl) {
|
|
536
|
+
// We capture pointer events only on this element; maintain list by querying PointerEvent.getCoalescedEvents not available broadly.
|
|
537
|
+
// Instead, rely on touches reported in native events; browsers will deliver move events for all active pointers targeting the element.
|
|
538
|
+
// Here we cannot query directly; so we return an empty array and use move handler state. For simplicity, we derive from event stream only.
|
|
539
|
+
// This helper remains for future extension; current logic passes actual events to compute pinch.
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
onWheel(evt, containerEl) {
|
|
543
|
+
const isCtrlZoom = evt.ctrlKey || evt.metaKey;
|
|
544
|
+
if (!isCtrlZoom)
|
|
545
|
+
return;
|
|
546
|
+
evt.preventDefault();
|
|
547
|
+
const rect = containerEl.getBoundingClientRect();
|
|
548
|
+
// Record gesture point relative to container
|
|
549
|
+
this.lastZoomClient = { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
|
|
550
|
+
if (this.wheelRaf)
|
|
551
|
+
cancelAnimationFrame(this.wheelRaf);
|
|
552
|
+
if (this.wheelCommitTimer)
|
|
553
|
+
clearTimeout(this.wheelCommitTimer);
|
|
554
|
+
const delta = -evt.deltaY;
|
|
555
|
+
const step = delta > 0 ? this.WHEEL_STEP : -this.WHEEL_STEP;
|
|
556
|
+
const desired = this.clampZoom(this.zoomSig() + step);
|
|
557
|
+
this.previewZoom(containerEl, desired);
|
|
558
|
+
this.wheelRaf = requestAnimationFrame(() => {
|
|
559
|
+
this.wheelCommitTimer = setTimeout(() => {
|
|
560
|
+
this.commitPreviewZoom(containerEl, this.zoomSig());
|
|
561
|
+
this.lastZoomClient = null;
|
|
562
|
+
}, this.gestureAnimationMs + 20);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
previewZoom(container, newZoom) {
|
|
566
|
+
const oldZoom = this.zoomSig();
|
|
567
|
+
if (Math.abs(newZoom - oldZoom) < 0.0001)
|
|
568
|
+
return;
|
|
569
|
+
const ratio = newZoom / oldZoom;
|
|
570
|
+
const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
|
|
571
|
+
const origin = this.getZoomAnimTarget(container);
|
|
572
|
+
target.style.willChange = 'transform';
|
|
573
|
+
target.style.transformOrigin = `${Math.max(0, origin.originX)}px ${Math.max(0, origin.originY)}px`;
|
|
574
|
+
target.style.transition = `transform ${this.gestureAnimationMs}ms cubic-bezier(0.2,0,0,1)`;
|
|
575
|
+
target.style.transform = `scale(${ratio})`;
|
|
576
|
+
this.zoomSig.set(newZoom);
|
|
577
|
+
this.previewActive = true;
|
|
578
|
+
}
|
|
579
|
+
async commitPreviewZoom(container, expectedZoom) {
|
|
580
|
+
// Clear transform and re-render once; cancel any in-flight render tasks
|
|
581
|
+
const target = this.continuous ? (container.querySelector('.pdf-content') || container) : (container.querySelector('canvas') || container);
|
|
582
|
+
target.style.transform = '';
|
|
583
|
+
target.style.willChange = '';
|
|
584
|
+
target.style.transition = '';
|
|
585
|
+
const anchor = this.captureScrollAnchor(container) || undefined;
|
|
586
|
+
if (this.previewActive) {
|
|
587
|
+
await this.safeReRenderWithCancellation(anchor);
|
|
588
|
+
this.previewActive = false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async safeReRenderWithCancellation(anchor) {
|
|
592
|
+
// If a render is in progress, queue once with the most recent anchor
|
|
593
|
+
if (this.renderLockCount > 0) {
|
|
594
|
+
this.reRenderQueued = true;
|
|
337
595
|
if (anchor)
|
|
338
|
-
this.
|
|
596
|
+
this.pendingAnchor = anchor;
|
|
339
597
|
return;
|
|
340
598
|
}
|
|
341
|
-
|
|
599
|
+
try {
|
|
600
|
+
this.renderLockCount++;
|
|
601
|
+
if (this.continuous) {
|
|
602
|
+
await this.renderAllPages(anchor);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
await this.render();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
this.renderLockCount = Math.max(0, this.renderLockCount - 1);
|
|
610
|
+
if (this.renderLockCount === 0 && this.reRenderQueued) {
|
|
611
|
+
const nextAnchor = this.pendingAnchor;
|
|
612
|
+
this.reRenderQueued = false;
|
|
613
|
+
this.pendingAnchor = undefined;
|
|
614
|
+
await this.safeReRenderWithCancellation(nextAnchor);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
342
617
|
}
|
|
343
618
|
captureScrollAnchor(container, client) {
|
|
344
619
|
try {
|
|
345
620
|
const pages = Array.from(container.querySelectorAll('.pdf-page'));
|
|
346
|
-
|
|
621
|
+
const centerY = container.scrollTop + container.clientHeight / 2;
|
|
622
|
+
if (pages.length > 0) {
|
|
623
|
+
let pageEl = pages.find(el => (el.offsetTop <= centerY && (el.offsetTop + el.offsetHeight) >= centerY))
|
|
624
|
+
|| pages.find(el => (el.offsetTop + el.offsetHeight) > (container.scrollTop + 1))
|
|
625
|
+
|| pages[0];
|
|
626
|
+
const page = parseInt(pageEl.getAttribute('data-page') || '1', 10);
|
|
627
|
+
const height = Math.max(1, pageEl.offsetHeight);
|
|
628
|
+
const ratio = Math.max(0, Math.min(1, (centerY - pageEl.offsetTop) / height));
|
|
629
|
+
return { page, ratio };
|
|
630
|
+
}
|
|
631
|
+
// Fallback: single-canvas mode
|
|
632
|
+
const canvas = container.querySelector('canvas');
|
|
633
|
+
if (!canvas)
|
|
347
634
|
return null;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
let pageEl = pages.find(el => (el.offsetTop <= centerY && (el.offsetTop + el.offsetHeight) >= centerY))
|
|
352
|
-
|| pages.find(el => (el.offsetTop + el.offsetHeight) > (container.scrollTop + 1))
|
|
353
|
-
|| pages[0];
|
|
354
|
-
const page = parseInt(pageEl.getAttribute('data-page') || '1', 10);
|
|
355
|
-
const height = Math.max(1, pageEl.offsetHeight);
|
|
356
|
-
const ratio = Math.max(0, Math.min(1, (centerY - pageEl.offsetTop) / height));
|
|
357
|
-
return { page, ratio };
|
|
635
|
+
const height = Math.max(1, canvas.offsetHeight);
|
|
636
|
+
const ratio = Math.max(0, Math.min(1, (centerY - canvas.offsetTop) / height));
|
|
637
|
+
return { page: 1, ratio };
|
|
358
638
|
}
|
|
359
639
|
catch {
|
|
360
640
|
return null;
|
|
@@ -362,13 +642,17 @@ class GenusPdfViewerComponent {
|
|
|
362
642
|
}
|
|
363
643
|
restoreScrollAnchor(container, anchor) {
|
|
364
644
|
try {
|
|
365
|
-
|
|
645
|
+
let el = container.querySelector(`[data-page="${anchor.page}"]`);
|
|
646
|
+
if (!el)
|
|
647
|
+
el = container.querySelector('canvas');
|
|
366
648
|
if (!el)
|
|
367
649
|
return;
|
|
368
650
|
const prevBehavior = container.style.scrollBehavior;
|
|
369
651
|
container.style.scrollBehavior = 'auto';
|
|
370
652
|
const height = Math.max(1, el.offsetHeight);
|
|
371
|
-
|
|
653
|
+
const desired = el.offsetTop + anchor.ratio * height - container.clientHeight / 2;
|
|
654
|
+
const maxScroll = Math.max(0, container.scrollHeight - container.clientHeight);
|
|
655
|
+
container.scrollTop = Math.max(0, Math.min(maxScroll, desired));
|
|
372
656
|
container.style.scrollBehavior = prevBehavior;
|
|
373
657
|
}
|
|
374
658
|
catch { }
|
|
@@ -376,31 +660,83 @@ class GenusPdfViewerComponent {
|
|
|
376
660
|
async download() {
|
|
377
661
|
const filename = this.deriveFilename();
|
|
378
662
|
const link = document.createElement('a');
|
|
663
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
664
|
+
let popup = null;
|
|
665
|
+
if (isIOS) {
|
|
666
|
+
// Open a tab synchronously to stay within user gesture
|
|
667
|
+
try {
|
|
668
|
+
popup = window.open('', '_blank', 'noopener,noreferrer');
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
popup = null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
379
674
|
const blob = await this.createBlobFromSrc();
|
|
380
675
|
if (blob) {
|
|
381
676
|
const url = URL.createObjectURL(blob);
|
|
382
|
-
|
|
383
|
-
|
|
677
|
+
if (isIOS) {
|
|
678
|
+
if (popup) {
|
|
679
|
+
try {
|
|
680
|
+
popup.location.href = url;
|
|
681
|
+
}
|
|
682
|
+
catch { /* ignore */ }
|
|
683
|
+
// Revoke later to allow the view to load fully
|
|
684
|
+
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const a = document.createElement('a');
|
|
688
|
+
a.href = url;
|
|
689
|
+
a.target = '_blank';
|
|
690
|
+
a.rel = 'noopener';
|
|
691
|
+
document.body.appendChild(a);
|
|
692
|
+
a.click();
|
|
693
|
+
a.remove();
|
|
694
|
+
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
link.href = url;
|
|
699
|
+
link.download = filename;
|
|
700
|
+
link.rel = 'noopener';
|
|
701
|
+
link.style.display = 'none';
|
|
702
|
+
document.body.appendChild(link);
|
|
703
|
+
link.click();
|
|
704
|
+
link.remove();
|
|
705
|
+
URL.revokeObjectURL(url);
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Fallback: cross-origin without CORS
|
|
710
|
+
const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
|
|
711
|
+
if (!href)
|
|
712
|
+
return;
|
|
713
|
+
if (isIOS) {
|
|
714
|
+
if (popup) {
|
|
715
|
+
try {
|
|
716
|
+
popup.location.href = href;
|
|
717
|
+
}
|
|
718
|
+
catch { /* ignore */ }
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
const a = document.createElement('a');
|
|
722
|
+
a.href = href;
|
|
723
|
+
a.target = '_blank';
|
|
724
|
+
a.rel = 'noopener';
|
|
725
|
+
document.body.appendChild(a);
|
|
726
|
+
a.click();
|
|
727
|
+
a.remove();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
link.href = href;
|
|
732
|
+
link.target = '_blank';
|
|
384
733
|
link.rel = 'noopener';
|
|
734
|
+
link.download = filename; // browsers may ignore for cross-origin
|
|
385
735
|
link.style.display = 'none';
|
|
386
736
|
document.body.appendChild(link);
|
|
387
737
|
link.click();
|
|
388
738
|
link.remove();
|
|
389
|
-
URL.revokeObjectURL(url);
|
|
390
|
-
return;
|
|
391
739
|
}
|
|
392
|
-
// Fallback: cross-origin without CORS; open in new tab rather than current window
|
|
393
|
-
const href = typeof this.src === 'string' ? this.src : (this.src instanceof URL ? this.src.toString() : '');
|
|
394
|
-
if (!href)
|
|
395
|
-
return;
|
|
396
|
-
link.href = href;
|
|
397
|
-
link.target = '_blank';
|
|
398
|
-
link.rel = 'noopener';
|
|
399
|
-
link.download = filename; // browsers may ignore for cross-origin
|
|
400
|
-
link.style.display = 'none';
|
|
401
|
-
document.body.appendChild(link);
|
|
402
|
-
link.click();
|
|
403
|
-
link.remove();
|
|
404
740
|
}
|
|
405
741
|
async print() {
|
|
406
742
|
// Robust, plugin-free printing by rendering pages to images
|
|
@@ -549,7 +885,7 @@ class GenusPdfViewerComponent {
|
|
|
549
885
|
return src;
|
|
550
886
|
}
|
|
551
887
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: GenusPdfViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
552
|
-
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" }, 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: `
|
|
888
|
+
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" }, 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: `
|
|
553
889
|
<div class="genus-pdf-shell">
|
|
554
890
|
<ng-content select="[toolbar]"></ng-content>
|
|
555
891
|
|
|
@@ -557,12 +893,13 @@ class GenusPdfViewerComponent {
|
|
|
557
893
|
<button type="button" title="Zoom out" aria-label="Zoom out" (click)="zoomOut()">−</button>
|
|
558
894
|
<span class="pct">{{ (zoomSig() * 100) | number:'1.0-0' }}%</span>
|
|
559
895
|
<button type="button" title="Zoom in" aria-label="Zoom in" (click)="zoomIn()">+</button>
|
|
896
|
+
<button type="button" *ngIf="isZoomed()" title="Reset zoom to 100%" aria-label="Reset zoom to 100%" (click)="resetZoom()">100%</button>
|
|
560
897
|
<span class="spacer"></span>
|
|
561
898
|
<button type="button" title="Previous page" aria-label="Previous page" (click)="prevPage()" [disabled]="pageSig() <= 1">⟨</button>
|
|
562
899
|
<span>{{ pageSig() }} / {{ doc ? doc.numPages : 0 }}</span>
|
|
563
900
|
<button type="button" title="Next page" aria-label="Next page" (click)="nextPage()" [disabled]="doc ? pageSig() >= doc.numPages : true">⟩</button>
|
|
564
901
|
<span class="spacer"></span>
|
|
565
|
-
<button type="button" title="Download" aria-label="Download" (click)="download()">
|
|
902
|
+
<button type="button" class="download-btn" title="Download" aria-label="Download" (click)="download()">
|
|
566
903
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
567
904
|
<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"/>
|
|
568
905
|
</svg>
|
|
@@ -570,15 +907,28 @@ class GenusPdfViewerComponent {
|
|
|
570
907
|
</button>
|
|
571
908
|
</div>
|
|
572
909
|
|
|
573
|
-
<div class="genus-pdf-stage" #singleStage [style.display]="continuous ? 'none' : 'flex'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, singleStage)" (scroll)="onScroll($event)">
|
|
910
|
+
<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)">
|
|
574
911
|
<div class="genus-pdf-overlay" *ngIf="loading()"><div class="spinner" aria-label="Loading"></div></div>
|
|
575
|
-
<div class="genus-pdf-overlay error" *ngIf="error()"
|
|
912
|
+
<div class="genus-pdf-overlay error" *ngIf="error()">
|
|
913
|
+
<div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
|
|
914
|
+
<span>{{ error() }}</span>
|
|
915
|
+
<button type="button" (click)="retryLoad()">Retry</button>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
576
918
|
<canvas #canvas></canvas>
|
|
577
919
|
</div>
|
|
578
920
|
|
|
579
|
-
<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)" (scroll)="onScroll($event)"
|
|
921
|
+
<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)">
|
|
922
|
+
<div class="genus-pdf-overlay" *ngIf="loading()"><div class="spinner" aria-label="Loading"></div></div>
|
|
923
|
+
<div class="genus-pdf-overlay error" *ngIf="error()">
|
|
924
|
+
<div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
|
|
925
|
+
<span>{{ error() }}</span>
|
|
926
|
+
<button type="button" (click)="retryLoad()">Retry</button>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
580
930
|
</div>
|
|
581
|
-
`, 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: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-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" }] });
|
|
931
|
+
`, 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-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" }] });
|
|
582
932
|
}
|
|
583
933
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: GenusPdfViewerComponent, decorators: [{
|
|
584
934
|
type: Component,
|
|
@@ -590,12 +940,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
590
940
|
<button type="button" title="Zoom out" aria-label="Zoom out" (click)="zoomOut()">−</button>
|
|
591
941
|
<span class="pct">{{ (zoomSig() * 100) | number:'1.0-0' }}%</span>
|
|
592
942
|
<button type="button" title="Zoom in" aria-label="Zoom in" (click)="zoomIn()">+</button>
|
|
943
|
+
<button type="button" *ngIf="isZoomed()" title="Reset zoom to 100%" aria-label="Reset zoom to 100%" (click)="resetZoom()">100%</button>
|
|
593
944
|
<span class="spacer"></span>
|
|
594
945
|
<button type="button" title="Previous page" aria-label="Previous page" (click)="prevPage()" [disabled]="pageSig() <= 1">⟨</button>
|
|
595
946
|
<span>{{ pageSig() }} / {{ doc ? doc.numPages : 0 }}</span>
|
|
596
947
|
<button type="button" title="Next page" aria-label="Next page" (click)="nextPage()" [disabled]="doc ? pageSig() >= doc.numPages : true">⟩</button>
|
|
597
948
|
<span class="spacer"></span>
|
|
598
|
-
<button type="button" title="Download" aria-label="Download" (click)="download()">
|
|
949
|
+
<button type="button" class="download-btn" title="Download" aria-label="Download" (click)="download()">
|
|
599
950
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
600
951
|
<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"/>
|
|
601
952
|
</svg>
|
|
@@ -603,15 +954,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
603
954
|
</button>
|
|
604
955
|
</div>
|
|
605
956
|
|
|
606
|
-
<div class="genus-pdf-stage" #singleStage [style.display]="continuous ? 'none' : 'flex'" [class.can-pan]="canPan()" [class.is-panning]="isPanning()" (pointerdown)="onPointerDown($event, singleStage)" (scroll)="onScroll($event)">
|
|
957
|
+
<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)">
|
|
607
958
|
<div class="genus-pdf-overlay" *ngIf="loading()"><div class="spinner" aria-label="Loading"></div></div>
|
|
608
|
-
<div class="genus-pdf-overlay error" *ngIf="error()"
|
|
959
|
+
<div class="genus-pdf-overlay error" *ngIf="error()">
|
|
960
|
+
<div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
|
|
961
|
+
<span>{{ error() }}</span>
|
|
962
|
+
<button type="button" (click)="retryLoad()">Retry</button>
|
|
963
|
+
</div>
|
|
964
|
+
</div>
|
|
609
965
|
<canvas #canvas></canvas>
|
|
610
966
|
</div>
|
|
611
967
|
|
|
612
|
-
<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)" (scroll)="onScroll($event)"
|
|
968
|
+
<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)">
|
|
969
|
+
<div class="genus-pdf-overlay" *ngIf="loading()"><div class="spinner" aria-label="Loading"></div></div>
|
|
970
|
+
<div class="genus-pdf-overlay error" *ngIf="error()">
|
|
971
|
+
<div style="display:flex;flex-direction:column;gap:8px;align-items:center;">
|
|
972
|
+
<span>{{ error() }}</span>
|
|
973
|
+
<button type="button" (click)="retryLoad()">Retry</button>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
613
977
|
</div>
|
|
614
|
-
`, 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: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-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"] }]
|
|
978
|
+
`, 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-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"] }]
|
|
615
979
|
}], ctorParameters: () => [], propDecorators: { src: [{
|
|
616
980
|
type: Input
|
|
617
981
|
}], page: [{
|
|
@@ -630,6 +994,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
|
|
|
630
994
|
type: Input
|
|
631
995
|
}], showToolbar: [{
|
|
632
996
|
type: Input
|
|
997
|
+
}], zoomAnimation: [{
|
|
998
|
+
type: Input
|
|
999
|
+
}], zoomAnimationMs: [{
|
|
1000
|
+
type: Input
|
|
1001
|
+
}], gestureAnimationMs: [{
|
|
1002
|
+
type: Input
|
|
633
1003
|
}], canvas: [{
|
|
634
1004
|
type: ViewChild,
|
|
635
1005
|
args: ['canvas', { static: false }]
|
|
@@ -646,23 +1016,24 @@ function initPdfWorkerFactory(cfg, platformId) {
|
|
|
646
1016
|
return () => {
|
|
647
1017
|
if (!isPlatformBrowser(platformId))
|
|
648
1018
|
return;
|
|
649
|
-
const
|
|
650
|
-
|
|
1019
|
+
const defaults = {
|
|
1020
|
+
tryModuleWorker: true,
|
|
1021
|
+
workerSrc: '/assets/pdf.worker.min.mjs',
|
|
1022
|
+
};
|
|
1023
|
+
const merged = { ...defaults, ...cfg };
|
|
1024
|
+
// 1) Try an ESM module worker (most robust across modern browsers)
|
|
651
1025
|
try {
|
|
652
|
-
if (tryModuleWorker && 'Worker' in window) {
|
|
1026
|
+
if (merged.tryModuleWorker && 'Worker' in window) {
|
|
653
1027
|
const worker = new Worker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url), { type: 'module' });
|
|
654
1028
|
pdfjsLib.GlobalWorkerOptions.workerPort = worker;
|
|
655
1029
|
return;
|
|
656
1030
|
}
|
|
657
1031
|
}
|
|
658
1032
|
catch {
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
// 2) Fallback: classic worker path (only if you explicitly pass one)
|
|
662
|
-
const workerSrc = cfg.workerSrc;
|
|
663
|
-
if (workerSrc) {
|
|
664
|
-
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
1033
|
+
// fall back below
|
|
665
1034
|
}
|
|
1035
|
+
// 2) Fallback: classic worker path served by the app
|
|
1036
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = merged.workerSrc;
|
|
666
1037
|
};
|
|
667
1038
|
}
|
|
668
1039
|
function provideGenusPdfViewer(cfg = {}) {
|