svelte-pdf-view 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +13 -0
- package/README.md +293 -0
- package/dist/PdfRenderer.svelte +238 -0
- package/dist/PdfRenderer.svelte.d.ts +21 -0
- package/dist/PdfToolbar.svelte +229 -0
- package/dist/PdfToolbar.svelte.d.ts +3 -0
- package/dist/PdfViewer.svelte +118 -0
- package/dist/PdfViewer.svelte.d.ts +17 -0
- package/dist/PdfViewerInner.svelte +302 -0
- package/dist/PdfViewerInner.svelte.d.ts +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/pdf-viewer/EventBus.d.ts +12 -0
- package/dist/pdf-viewer/EventBus.js +42 -0
- package/dist/pdf-viewer/FindController.d.ts +53 -0
- package/dist/pdf-viewer/FindController.js +423 -0
- package/dist/pdf-viewer/PDFPageView.d.ts +58 -0
- package/dist/pdf-viewer/PDFPageView.js +281 -0
- package/dist/pdf-viewer/PDFViewerCore.d.ts +45 -0
- package/dist/pdf-viewer/PDFViewerCore.js +225 -0
- package/dist/pdf-viewer/context.d.ts +31 -0
- package/dist/pdf-viewer/context.js +15 -0
- package/dist/pdf-viewer/renderer-styles.css +203 -0
- package/dist/pdf-viewer/styles.css +281 -0
- package/package.json +88 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/* Copyright 2024 Mozilla Foundation
|
|
2
|
+
*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
import { setLayerDimensions } from 'pdfjs-dist';
|
|
16
|
+
export const RenderingStates = {
|
|
17
|
+
INITIAL: 0,
|
|
18
|
+
RUNNING: 1,
|
|
19
|
+
PAUSED: 2,
|
|
20
|
+
FINISHED: 3
|
|
21
|
+
};
|
|
22
|
+
export class PDFPageView {
|
|
23
|
+
id;
|
|
24
|
+
eventBus;
|
|
25
|
+
container;
|
|
26
|
+
viewport;
|
|
27
|
+
pdfPage = null;
|
|
28
|
+
scale;
|
|
29
|
+
rotation;
|
|
30
|
+
pdfPageRotate = 0;
|
|
31
|
+
div;
|
|
32
|
+
canvas = null;
|
|
33
|
+
canvasWrapper = null;
|
|
34
|
+
textLayerDiv = null;
|
|
35
|
+
loadingDiv = null;
|
|
36
|
+
renderingState = RenderingStates.INITIAL;
|
|
37
|
+
renderTask = null;
|
|
38
|
+
// Text layer instance for updates
|
|
39
|
+
textLayer = null;
|
|
40
|
+
textLayerRendered = false;
|
|
41
|
+
// Text layer data for search
|
|
42
|
+
textDivs = [];
|
|
43
|
+
textContentItemsStr = [];
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.id = options.id;
|
|
46
|
+
this.container = options.container;
|
|
47
|
+
this.eventBus = options.eventBus;
|
|
48
|
+
this.scale = options.scale ?? 1.0;
|
|
49
|
+
this.rotation = options.rotation ?? 0;
|
|
50
|
+
this.viewport = options.defaultViewport;
|
|
51
|
+
// Create page container
|
|
52
|
+
this.div = document.createElement('div');
|
|
53
|
+
this.div.className = 'page';
|
|
54
|
+
this.div.setAttribute('data-page-number', String(this.id));
|
|
55
|
+
this.div.setAttribute('role', 'region');
|
|
56
|
+
this.setDimensions();
|
|
57
|
+
this.container.appendChild(this.div);
|
|
58
|
+
// Add loading indicator
|
|
59
|
+
this.loadingDiv = document.createElement('div');
|
|
60
|
+
this.loadingDiv.className = 'loadingIcon';
|
|
61
|
+
this.loadingDiv.textContent = 'Loading...';
|
|
62
|
+
this.div.appendChild(this.loadingDiv);
|
|
63
|
+
}
|
|
64
|
+
setDimensions() {
|
|
65
|
+
const { width, height } = this.viewport;
|
|
66
|
+
this.div.style.width = `${Math.floor(width)}px`;
|
|
67
|
+
this.div.style.height = `${Math.floor(height)}px`;
|
|
68
|
+
// Set CSS variables for text layer scaling
|
|
69
|
+
// viewport.scale already includes our scale factor
|
|
70
|
+
this.div.style.setProperty('--scale-factor', String(this.viewport.scale));
|
|
71
|
+
// Set rotation attribute for text layer
|
|
72
|
+
const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
|
|
73
|
+
this.div.setAttribute('data-main-rotation', String(totalRotation));
|
|
74
|
+
// Update text layer dimensions if it exists
|
|
75
|
+
// mustFlip=false because the text layer uses raw page coordinates
|
|
76
|
+
// and rotation is handled via CSS transforms
|
|
77
|
+
if (this.textLayerDiv) {
|
|
78
|
+
setLayerDimensions(this.textLayerDiv, this.viewport, /* mustFlip */ false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
setPdfPage(pdfPage) {
|
|
82
|
+
this.pdfPage = pdfPage;
|
|
83
|
+
this.pdfPageRotate = pdfPage.rotate;
|
|
84
|
+
this.updateViewport();
|
|
85
|
+
}
|
|
86
|
+
updateViewport() {
|
|
87
|
+
if (!this.pdfPage)
|
|
88
|
+
return;
|
|
89
|
+
const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
|
|
90
|
+
this.viewport = this.pdfPage.getViewport({
|
|
91
|
+
scale: this.scale,
|
|
92
|
+
rotation: totalRotation
|
|
93
|
+
});
|
|
94
|
+
this.setDimensions();
|
|
95
|
+
}
|
|
96
|
+
update({ scale, rotation }) {
|
|
97
|
+
if (scale !== undefined) {
|
|
98
|
+
this.scale = scale;
|
|
99
|
+
}
|
|
100
|
+
if (rotation !== undefined) {
|
|
101
|
+
this.rotation = rotation;
|
|
102
|
+
}
|
|
103
|
+
this.updateViewport();
|
|
104
|
+
// Re-render if already rendered
|
|
105
|
+
if (this.renderingState === RenderingStates.FINISHED) {
|
|
106
|
+
// Update text layer - TextLayer.update() handles both scale and rotation
|
|
107
|
+
// Rotation is applied via CSS transforms based on data-main-rotation attribute
|
|
108
|
+
if (this.textLayer && this.textLayerRendered) {
|
|
109
|
+
this.textLayerDiv.hidden = true;
|
|
110
|
+
this.textLayer.update({
|
|
111
|
+
viewport: this.viewport
|
|
112
|
+
});
|
|
113
|
+
this.textLayerDiv.hidden = false;
|
|
114
|
+
}
|
|
115
|
+
// Re-render canvas
|
|
116
|
+
this.resetCanvas();
|
|
117
|
+
this.draw();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
resetCanvas() {
|
|
121
|
+
this.cancelRendering();
|
|
122
|
+
this.renderingState = RenderingStates.INITIAL;
|
|
123
|
+
// Clear canvas only
|
|
124
|
+
if (this.canvas) {
|
|
125
|
+
this.canvas.width = 0;
|
|
126
|
+
this.canvas.height = 0;
|
|
127
|
+
this.canvas.remove();
|
|
128
|
+
this.canvas = null;
|
|
129
|
+
}
|
|
130
|
+
if (this.canvasWrapper) {
|
|
131
|
+
this.canvasWrapper.remove();
|
|
132
|
+
this.canvasWrapper = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
reset() {
|
|
136
|
+
this.cancelRendering();
|
|
137
|
+
this.renderingState = RenderingStates.INITIAL;
|
|
138
|
+
// Clear canvas
|
|
139
|
+
if (this.canvas) {
|
|
140
|
+
this.canvas.width = 0;
|
|
141
|
+
this.canvas.height = 0;
|
|
142
|
+
this.canvas.remove();
|
|
143
|
+
this.canvas = null;
|
|
144
|
+
}
|
|
145
|
+
if (this.canvasWrapper) {
|
|
146
|
+
this.canvasWrapper.remove();
|
|
147
|
+
this.canvasWrapper = null;
|
|
148
|
+
}
|
|
149
|
+
// Clear text layer
|
|
150
|
+
if (this.textLayerDiv) {
|
|
151
|
+
this.textLayerDiv.remove();
|
|
152
|
+
this.textLayerDiv = null;
|
|
153
|
+
}
|
|
154
|
+
this.textLayer = null;
|
|
155
|
+
this.textLayerRendered = false;
|
|
156
|
+
this.textDivs = [];
|
|
157
|
+
this.textContentItemsStr = [];
|
|
158
|
+
// Show loading
|
|
159
|
+
if (this.loadingDiv) {
|
|
160
|
+
this.loadingDiv.style.display = '';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async draw() {
|
|
164
|
+
if (!this.pdfPage || this.renderingState !== RenderingStates.INITIAL) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.renderingState = RenderingStates.RUNNING;
|
|
168
|
+
try {
|
|
169
|
+
// Hide loading indicator
|
|
170
|
+
if (this.loadingDiv) {
|
|
171
|
+
this.loadingDiv.style.display = 'none';
|
|
172
|
+
}
|
|
173
|
+
// Create canvas wrapper
|
|
174
|
+
this.canvasWrapper = document.createElement('div');
|
|
175
|
+
this.canvasWrapper.className = 'canvasWrapper';
|
|
176
|
+
this.div.appendChild(this.canvasWrapper);
|
|
177
|
+
// Create and render canvas
|
|
178
|
+
this.canvas = document.createElement('canvas');
|
|
179
|
+
this.canvas.className = 'pdf-canvas';
|
|
180
|
+
this.canvasWrapper.appendChild(this.canvas);
|
|
181
|
+
const outputScale = window.devicePixelRatio || 1;
|
|
182
|
+
const { width, height } = this.viewport;
|
|
183
|
+
this.canvas.width = Math.floor(width * outputScale);
|
|
184
|
+
this.canvas.height = Math.floor(height * outputScale);
|
|
185
|
+
this.canvas.style.width = `${Math.floor(width)}px`;
|
|
186
|
+
this.canvas.style.height = `${Math.floor(height)}px`;
|
|
187
|
+
const ctx = this.canvas.getContext('2d');
|
|
188
|
+
ctx.scale(outputScale, outputScale);
|
|
189
|
+
this.renderTask = this.pdfPage.render({
|
|
190
|
+
canvasContext: ctx,
|
|
191
|
+
viewport: this.viewport,
|
|
192
|
+
canvas: this.canvas
|
|
193
|
+
});
|
|
194
|
+
await this.renderTask.promise;
|
|
195
|
+
this.renderTask = null;
|
|
196
|
+
// Render text layer for search (only if not already rendered)
|
|
197
|
+
if (!this.textLayerRendered) {
|
|
198
|
+
await this.renderTextLayer();
|
|
199
|
+
}
|
|
200
|
+
this.renderingState = RenderingStates.FINISHED;
|
|
201
|
+
this.eventBus.dispatch('pagerendered', {
|
|
202
|
+
pageNumber: this.id,
|
|
203
|
+
source: this
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error.name === 'RenderingCancelledException') {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.renderingState = RenderingStates.INITIAL;
|
|
211
|
+
console.error('Error rendering page:', error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async renderTextLayer() {
|
|
215
|
+
if (!this.pdfPage)
|
|
216
|
+
return;
|
|
217
|
+
// If text layer already rendered, just update it
|
|
218
|
+
if (this.textLayerRendered && this.textLayer) {
|
|
219
|
+
this.textLayerDiv.hidden = true;
|
|
220
|
+
this.textLayer.update({ viewport: this.viewport });
|
|
221
|
+
this.textLayerDiv.hidden = false;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Create text layer div
|
|
225
|
+
this.textLayerDiv = document.createElement('div');
|
|
226
|
+
this.textLayerDiv.className = 'textLayer';
|
|
227
|
+
this.div.appendChild(this.textLayerDiv);
|
|
228
|
+
try {
|
|
229
|
+
// Import TextLayer from pdfjs-dist
|
|
230
|
+
const { TextLayer } = await import('pdfjs-dist');
|
|
231
|
+
const textContent = await this.pdfPage.getTextContent();
|
|
232
|
+
this.textDivs = [];
|
|
233
|
+
this.textContentItemsStr = [];
|
|
234
|
+
// Set text layer dimensions using PDF.js utility
|
|
235
|
+
// mustFlip=false because text layer uses raw page coordinates
|
|
236
|
+
// and rotation is handled via CSS transforms
|
|
237
|
+
setLayerDimensions(this.textLayerDiv, this.viewport, /* mustFlip */ false);
|
|
238
|
+
// Use PDF.js TextLayer for proper positioning
|
|
239
|
+
this.textLayer = new TextLayer({
|
|
240
|
+
textContentSource: textContent,
|
|
241
|
+
container: this.textLayerDiv,
|
|
242
|
+
viewport: this.viewport
|
|
243
|
+
});
|
|
244
|
+
await this.textLayer.render();
|
|
245
|
+
this.textLayerRendered = true;
|
|
246
|
+
// Collect text divs and their content for search highlighting
|
|
247
|
+
// We extract text from the rendered spans to ensure 1:1 correspondence
|
|
248
|
+
// between textDivs and textContentItemsStr
|
|
249
|
+
const spans = this.textLayerDiv.querySelectorAll('span:not(.markedContent)');
|
|
250
|
+
spans.forEach((span) => {
|
|
251
|
+
this.textDivs.push(span);
|
|
252
|
+
this.textContentItemsStr.push(span.textContent || '');
|
|
253
|
+
});
|
|
254
|
+
this.eventBus.dispatch('textlayerrendered', {
|
|
255
|
+
pageNumber: this.id,
|
|
256
|
+
source: this
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
console.error('Error rendering text layer:', error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
cancelRendering() {
|
|
264
|
+
if (this.renderTask) {
|
|
265
|
+
this.renderTask.cancel();
|
|
266
|
+
this.renderTask = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
destroy() {
|
|
270
|
+
this.cancelRendering();
|
|
271
|
+
this.reset();
|
|
272
|
+
this.pdfPage?.cleanup();
|
|
273
|
+
this.div.remove();
|
|
274
|
+
}
|
|
275
|
+
get width() {
|
|
276
|
+
return this.viewport.width;
|
|
277
|
+
}
|
|
278
|
+
get height() {
|
|
279
|
+
return this.viewport.height;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDFViewerCore - Main viewer that manages all pages in a scroll container.
|
|
3
|
+
* This is a derivative work based on PDF.js pdf_viewer.js
|
|
4
|
+
*/
|
|
5
|
+
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
6
|
+
import { EventBus } from './EventBus.js';
|
|
7
|
+
import { PDFPageView } from './PDFPageView.js';
|
|
8
|
+
export interface PDFViewerOptions {
|
|
9
|
+
container: HTMLElement;
|
|
10
|
+
eventBus?: EventBus;
|
|
11
|
+
initialScale?: number;
|
|
12
|
+
initialRotation?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class PDFViewerCore {
|
|
15
|
+
readonly container: HTMLElement;
|
|
16
|
+
readonly viewer: HTMLDivElement;
|
|
17
|
+
readonly eventBus: EventBus;
|
|
18
|
+
private pdfDocument;
|
|
19
|
+
private pages;
|
|
20
|
+
private currentScale;
|
|
21
|
+
private currentRotation;
|
|
22
|
+
private scrollAbortController;
|
|
23
|
+
private renderingQueue;
|
|
24
|
+
private isRendering;
|
|
25
|
+
constructor(options: PDFViewerOptions);
|
|
26
|
+
private setupScrollListener;
|
|
27
|
+
setDocument(pdfDocument: PDFDocumentProxy): Promise<void>;
|
|
28
|
+
private getVisiblePages;
|
|
29
|
+
private updateVisiblePages;
|
|
30
|
+
private processRenderingQueue;
|
|
31
|
+
get scale(): number;
|
|
32
|
+
set scale(value: number);
|
|
33
|
+
get rotation(): number;
|
|
34
|
+
set rotation(value: number);
|
|
35
|
+
zoomIn(): void;
|
|
36
|
+
zoomOut(): void;
|
|
37
|
+
rotateClockwise(): void;
|
|
38
|
+
rotateCounterClockwise(): void;
|
|
39
|
+
scrollToPage(pageNumber: number): void;
|
|
40
|
+
get pagesCount(): number;
|
|
41
|
+
get currentPageNumber(): number;
|
|
42
|
+
getPageView(pageIndex: number): PDFPageView | undefined;
|
|
43
|
+
cleanup(): void;
|
|
44
|
+
destroy(): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* Copyright 2024 Mozilla Foundation
|
|
2
|
+
*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
import { EventBus } from './EventBus.js';
|
|
16
|
+
import { PDFPageView, RenderingStates } from './PDFPageView.js';
|
|
17
|
+
const DEFAULT_SCALE = 1.0;
|
|
18
|
+
const MIN_SCALE = 0.1;
|
|
19
|
+
const MAX_SCALE = 10.0;
|
|
20
|
+
const DEFAULT_SCALE_DELTA = 0.1;
|
|
21
|
+
// Number of pages to render around the visible ones
|
|
22
|
+
const PAGES_TO_PRERENDER = 2;
|
|
23
|
+
export class PDFViewerCore {
|
|
24
|
+
container;
|
|
25
|
+
viewer;
|
|
26
|
+
eventBus;
|
|
27
|
+
pdfDocument = null;
|
|
28
|
+
pages = [];
|
|
29
|
+
currentScale;
|
|
30
|
+
currentRotation;
|
|
31
|
+
scrollAbortController = null;
|
|
32
|
+
renderingQueue = new Set();
|
|
33
|
+
isRendering = false;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.container = options.container;
|
|
36
|
+
this.eventBus = options.eventBus ?? new EventBus();
|
|
37
|
+
this.currentScale = options.initialScale ?? DEFAULT_SCALE;
|
|
38
|
+
this.currentRotation = options.initialRotation ?? 0;
|
|
39
|
+
// Create viewer div inside container
|
|
40
|
+
this.viewer = document.createElement('div');
|
|
41
|
+
this.viewer.className = 'pdfViewer';
|
|
42
|
+
this.container.appendChild(this.viewer);
|
|
43
|
+
// Setup scroll listener for lazy rendering
|
|
44
|
+
this.setupScrollListener();
|
|
45
|
+
}
|
|
46
|
+
setupScrollListener() {
|
|
47
|
+
this.scrollAbortController?.abort();
|
|
48
|
+
this.scrollAbortController = new AbortController();
|
|
49
|
+
let scrollTimeout = null;
|
|
50
|
+
this.container.addEventListener('scroll', () => {
|
|
51
|
+
if (scrollTimeout) {
|
|
52
|
+
clearTimeout(scrollTimeout);
|
|
53
|
+
}
|
|
54
|
+
scrollTimeout = setTimeout(() => {
|
|
55
|
+
this.updateVisiblePages();
|
|
56
|
+
}, 100);
|
|
57
|
+
}, { signal: this.scrollAbortController.signal });
|
|
58
|
+
}
|
|
59
|
+
async setDocument(pdfDocument) {
|
|
60
|
+
// Cleanup previous document
|
|
61
|
+
this.cleanup();
|
|
62
|
+
this.pdfDocument = pdfDocument;
|
|
63
|
+
const numPages = pdfDocument.numPages;
|
|
64
|
+
// Create page views
|
|
65
|
+
for (let i = 1; i <= numPages; i++) {
|
|
66
|
+
const page = await pdfDocument.getPage(i);
|
|
67
|
+
const viewport = page.getViewport({
|
|
68
|
+
scale: this.currentScale,
|
|
69
|
+
rotation: this.currentRotation
|
|
70
|
+
});
|
|
71
|
+
const pageView = new PDFPageView({
|
|
72
|
+
container: this.viewer,
|
|
73
|
+
id: i,
|
|
74
|
+
defaultViewport: viewport,
|
|
75
|
+
eventBus: this.eventBus,
|
|
76
|
+
scale: this.currentScale,
|
|
77
|
+
rotation: this.currentRotation
|
|
78
|
+
});
|
|
79
|
+
pageView.setPdfPage(page);
|
|
80
|
+
this.pages.push(pageView);
|
|
81
|
+
}
|
|
82
|
+
this.eventBus.dispatch('pagesloaded', { pagesCount: numPages });
|
|
83
|
+
// Initial render of visible pages
|
|
84
|
+
this.updateVisiblePages();
|
|
85
|
+
}
|
|
86
|
+
getVisiblePages() {
|
|
87
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
88
|
+
const containerTop = this.container.scrollTop;
|
|
89
|
+
const containerBottom = containerTop + containerRect.height;
|
|
90
|
+
let firstVisible = -1;
|
|
91
|
+
let lastVisible = -1;
|
|
92
|
+
const visibleIds = new Set();
|
|
93
|
+
let currentTop = 0;
|
|
94
|
+
for (let i = 0; i < this.pages.length; i++) {
|
|
95
|
+
const page = this.pages[i];
|
|
96
|
+
const pageBottom = currentTop + page.height + 10; // 10px margin
|
|
97
|
+
if (pageBottom >= containerTop && currentTop <= containerBottom) {
|
|
98
|
+
if (firstVisible === -1) {
|
|
99
|
+
firstVisible = i;
|
|
100
|
+
}
|
|
101
|
+
lastVisible = i;
|
|
102
|
+
visibleIds.add(i + 1); // Page numbers are 1-indexed
|
|
103
|
+
}
|
|
104
|
+
currentTop = pageBottom;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
first: firstVisible === -1 ? 0 : firstVisible,
|
|
108
|
+
last: lastVisible === -1 ? 0 : lastVisible,
|
|
109
|
+
ids: visibleIds
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
updateVisiblePages() {
|
|
113
|
+
if (!this.pdfDocument || this.pages.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const visible = this.getVisiblePages();
|
|
116
|
+
// Calculate which pages to render (visible + buffer)
|
|
117
|
+
const startPage = Math.max(0, visible.first - PAGES_TO_PRERENDER);
|
|
118
|
+
const endPage = Math.min(this.pages.length - 1, visible.last + PAGES_TO_PRERENDER);
|
|
119
|
+
// Queue pages for rendering
|
|
120
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
121
|
+
const page = this.pages[i];
|
|
122
|
+
if (page.renderingState === RenderingStates.INITIAL) {
|
|
123
|
+
this.renderingQueue.add(i);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this.processRenderingQueue();
|
|
127
|
+
this.eventBus.dispatch('updateviewarea', {
|
|
128
|
+
location: {
|
|
129
|
+
pageNumber: visible.first + 1,
|
|
130
|
+
scale: this.currentScale,
|
|
131
|
+
rotation: this.currentRotation
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async processRenderingQueue() {
|
|
136
|
+
if (this.isRendering || this.renderingQueue.size === 0)
|
|
137
|
+
return;
|
|
138
|
+
this.isRendering = true;
|
|
139
|
+
while (this.renderingQueue.size > 0) {
|
|
140
|
+
const pageIndex = this.renderingQueue.values().next().value;
|
|
141
|
+
this.renderingQueue.delete(pageIndex);
|
|
142
|
+
const page = this.pages[pageIndex];
|
|
143
|
+
if (page && page.renderingState === RenderingStates.INITIAL) {
|
|
144
|
+
await page.draw();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.isRendering = false;
|
|
148
|
+
}
|
|
149
|
+
get scale() {
|
|
150
|
+
return this.currentScale;
|
|
151
|
+
}
|
|
152
|
+
set scale(value) {
|
|
153
|
+
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, value));
|
|
154
|
+
if (newScale === this.currentScale)
|
|
155
|
+
return;
|
|
156
|
+
this.currentScale = newScale;
|
|
157
|
+
// Update all pages
|
|
158
|
+
for (const page of this.pages) {
|
|
159
|
+
page.update({ scale: newScale });
|
|
160
|
+
}
|
|
161
|
+
this.eventBus.dispatch('scalechanged', { scale: newScale });
|
|
162
|
+
this.updateVisiblePages();
|
|
163
|
+
}
|
|
164
|
+
get rotation() {
|
|
165
|
+
return this.currentRotation;
|
|
166
|
+
}
|
|
167
|
+
set rotation(value) {
|
|
168
|
+
const newRotation = ((value % 360) + 360) % 360;
|
|
169
|
+
if (newRotation === this.currentRotation)
|
|
170
|
+
return;
|
|
171
|
+
this.currentRotation = newRotation;
|
|
172
|
+
// Update all pages
|
|
173
|
+
for (const page of this.pages) {
|
|
174
|
+
page.update({ rotation: newRotation });
|
|
175
|
+
}
|
|
176
|
+
this.eventBus.dispatch('rotationchanged', { rotation: newRotation });
|
|
177
|
+
this.updateVisiblePages();
|
|
178
|
+
}
|
|
179
|
+
zoomIn() {
|
|
180
|
+
this.scale = this.currentScale + DEFAULT_SCALE_DELTA;
|
|
181
|
+
}
|
|
182
|
+
zoomOut() {
|
|
183
|
+
this.scale = this.currentScale - DEFAULT_SCALE_DELTA;
|
|
184
|
+
}
|
|
185
|
+
rotateClockwise() {
|
|
186
|
+
this.rotation = this.currentRotation + 90;
|
|
187
|
+
}
|
|
188
|
+
rotateCounterClockwise() {
|
|
189
|
+
this.rotation = this.currentRotation - 90;
|
|
190
|
+
}
|
|
191
|
+
scrollToPage(pageNumber) {
|
|
192
|
+
if (pageNumber < 1 || pageNumber > this.pages.length)
|
|
193
|
+
return;
|
|
194
|
+
const pageView = this.pages[pageNumber - 1];
|
|
195
|
+
pageView.div.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
196
|
+
this.eventBus.dispatch('pagechanged', { pageNumber });
|
|
197
|
+
}
|
|
198
|
+
get pagesCount() {
|
|
199
|
+
return this.pages.length;
|
|
200
|
+
}
|
|
201
|
+
get currentPageNumber() {
|
|
202
|
+
const visible = this.getVisiblePages();
|
|
203
|
+
return visible.first + 1;
|
|
204
|
+
}
|
|
205
|
+
getPageView(pageIndex) {
|
|
206
|
+
return this.pages[pageIndex];
|
|
207
|
+
}
|
|
208
|
+
cleanup() {
|
|
209
|
+
// Cancel all rendering
|
|
210
|
+
for (const page of this.pages) {
|
|
211
|
+
page.destroy();
|
|
212
|
+
}
|
|
213
|
+
this.pages = [];
|
|
214
|
+
this.renderingQueue.clear();
|
|
215
|
+
// Clear viewer
|
|
216
|
+
this.viewer.innerHTML = '';
|
|
217
|
+
this.pdfDocument = null;
|
|
218
|
+
}
|
|
219
|
+
destroy() {
|
|
220
|
+
this.scrollAbortController?.abort();
|
|
221
|
+
this.cleanup();
|
|
222
|
+
this.eventBus.destroy();
|
|
223
|
+
this.viewer.remove();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface PdfViewerState {
|
|
2
|
+
loading: boolean;
|
|
3
|
+
error: string | null;
|
|
4
|
+
totalPages: number;
|
|
5
|
+
currentPage: number;
|
|
6
|
+
scale: number;
|
|
7
|
+
rotation: number;
|
|
8
|
+
searchQuery: string;
|
|
9
|
+
searchCurrent: number;
|
|
10
|
+
searchTotal: number;
|
|
11
|
+
isSearching: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface PdfViewerActions {
|
|
14
|
+
zoomIn: () => void;
|
|
15
|
+
zoomOut: () => void;
|
|
16
|
+
setScale: (scale: number) => void;
|
|
17
|
+
rotateClockwise: () => void;
|
|
18
|
+
rotateCounterClockwise: () => void;
|
|
19
|
+
goToPage: (page: number) => void;
|
|
20
|
+
search: (query: string) => Promise<void>;
|
|
21
|
+
searchNext: () => void;
|
|
22
|
+
searchPrevious: () => void;
|
|
23
|
+
clearSearch: () => void;
|
|
24
|
+
}
|
|
25
|
+
export interface PdfViewerContext {
|
|
26
|
+
state: PdfViewerState;
|
|
27
|
+
actions: PdfViewerActions;
|
|
28
|
+
_registerRenderer: (renderer: PdfViewerActions) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function setPdfViewerContext(ctx: PdfViewerContext): void;
|
|
31
|
+
export declare function getPdfViewerContext(): PdfViewerContext;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF Viewer Context - Shared state between toolbar and renderer
|
|
3
|
+
*/
|
|
4
|
+
import { getContext, setContext } from 'svelte';
|
|
5
|
+
const PDF_VIEWER_CONTEXT_KEY = Symbol('pdf-viewer');
|
|
6
|
+
export function setPdfViewerContext(ctx) {
|
|
7
|
+
setContext(PDF_VIEWER_CONTEXT_KEY, ctx);
|
|
8
|
+
}
|
|
9
|
+
export function getPdfViewerContext() {
|
|
10
|
+
const ctx = getContext(PDF_VIEWER_CONTEXT_KEY);
|
|
11
|
+
if (!ctx) {
|
|
12
|
+
throw new Error('PdfToolbar must be used inside a PdfViewer component');
|
|
13
|
+
}
|
|
14
|
+
return ctx;
|
|
15
|
+
}
|