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,42 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* Simple event bus for PDF viewer component communication.
|
|
17
|
+
* This is a derivative work based on PDF.js event_utils.js
|
|
18
|
+
*/
|
|
19
|
+
export class EventBus {
|
|
20
|
+
listeners = new Map();
|
|
21
|
+
on(eventName, listener) {
|
|
22
|
+
if (!this.listeners.has(eventName)) {
|
|
23
|
+
this.listeners.set(eventName, new Set());
|
|
24
|
+
}
|
|
25
|
+
this.listeners.get(eventName).add(listener);
|
|
26
|
+
}
|
|
27
|
+
off(eventName, listener) {
|
|
28
|
+
this.listeners.get(eventName)?.delete(listener);
|
|
29
|
+
}
|
|
30
|
+
dispatch(eventName, data) {
|
|
31
|
+
const eventListeners = this.listeners.get(eventName);
|
|
32
|
+
if (!eventListeners || eventListeners.size === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const listener of eventListeners) {
|
|
36
|
+
listener({ source: this, ...data });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
destroy() {
|
|
40
|
+
this.listeners.clear();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FindController - Text search functionality for PDF viewer.
|
|
3
|
+
* This is a derivative work based on PDF.js pdf_find_controller.js and text_highlighter.js
|
|
4
|
+
*/
|
|
5
|
+
import type { EventBus } from './EventBus.js';
|
|
6
|
+
import type { PDFViewerCore } from './PDFViewerCore.js';
|
|
7
|
+
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
8
|
+
export declare const FindState: {
|
|
9
|
+
readonly FOUND: 0;
|
|
10
|
+
readonly NOT_FOUND: 1;
|
|
11
|
+
readonly WRAPPED: 2;
|
|
12
|
+
readonly PENDING: 3;
|
|
13
|
+
};
|
|
14
|
+
export interface FindOptions {
|
|
15
|
+
query: string;
|
|
16
|
+
highlightAll?: boolean;
|
|
17
|
+
caseSensitive?: boolean;
|
|
18
|
+
entireWord?: boolean;
|
|
19
|
+
findPrevious?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class FindController {
|
|
22
|
+
private viewer;
|
|
23
|
+
private eventBus;
|
|
24
|
+
private pdfDocument;
|
|
25
|
+
private query;
|
|
26
|
+
private caseSensitive;
|
|
27
|
+
private entireWord;
|
|
28
|
+
private highlightAll;
|
|
29
|
+
private pageContents;
|
|
30
|
+
private extractTextPromises;
|
|
31
|
+
private pageMatches;
|
|
32
|
+
private pageMatchesLength;
|
|
33
|
+
private allMatches;
|
|
34
|
+
private selectedMatchIndex;
|
|
35
|
+
private matchesCountTotal;
|
|
36
|
+
constructor(viewer: PDFViewerCore, eventBus: EventBus);
|
|
37
|
+
setDocument(pdfDocument: PDFDocumentProxy): void;
|
|
38
|
+
private extractText;
|
|
39
|
+
find(options: FindOptions): Promise<void>;
|
|
40
|
+
findNext(): void;
|
|
41
|
+
findPrevious(): void;
|
|
42
|
+
private highlightAllPages;
|
|
43
|
+
private highlightPage;
|
|
44
|
+
private convertMatches;
|
|
45
|
+
private renderMatches;
|
|
46
|
+
private scrollToSelectedMatch;
|
|
47
|
+
private clearHighlights;
|
|
48
|
+
get matchesCount(): {
|
|
49
|
+
current: number;
|
|
50
|
+
total: number;
|
|
51
|
+
};
|
|
52
|
+
reset(): void;
|
|
53
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/* Copyright 2024 Mozilla Foundation
|
|
2
|
+
*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
export const FindState = {
|
|
16
|
+
FOUND: 0,
|
|
17
|
+
NOT_FOUND: 1,
|
|
18
|
+
WRAPPED: 2,
|
|
19
|
+
PENDING: 3
|
|
20
|
+
};
|
|
21
|
+
export class FindController {
|
|
22
|
+
viewer;
|
|
23
|
+
eventBus;
|
|
24
|
+
pdfDocument = null;
|
|
25
|
+
query = '';
|
|
26
|
+
caseSensitive = false;
|
|
27
|
+
entireWord = false;
|
|
28
|
+
highlightAll = true;
|
|
29
|
+
// Page text content extracted directly from PDF
|
|
30
|
+
pageContents = [];
|
|
31
|
+
extractTextPromises = [];
|
|
32
|
+
// Matches per page
|
|
33
|
+
pageMatches = [];
|
|
34
|
+
pageMatchesLength = [];
|
|
35
|
+
// Flattened matches for navigation
|
|
36
|
+
allMatches = [];
|
|
37
|
+
selectedMatchIndex = -1;
|
|
38
|
+
matchesCountTotal = 0;
|
|
39
|
+
constructor(viewer, eventBus) {
|
|
40
|
+
this.viewer = viewer;
|
|
41
|
+
this.eventBus = eventBus;
|
|
42
|
+
}
|
|
43
|
+
setDocument(pdfDocument) {
|
|
44
|
+
this.pdfDocument = pdfDocument;
|
|
45
|
+
this.pageContents = [];
|
|
46
|
+
this.extractTextPromises = [];
|
|
47
|
+
this.reset();
|
|
48
|
+
}
|
|
49
|
+
async extractText() {
|
|
50
|
+
if (!this.pdfDocument || this.extractTextPromises.length > 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const numPages = this.pdfDocument.numPages;
|
|
54
|
+
for (let i = 0; i < numPages; i++) {
|
|
55
|
+
const promise = (async () => {
|
|
56
|
+
try {
|
|
57
|
+
const page = await this.pdfDocument.getPage(i + 1);
|
|
58
|
+
// Don't use disableNormalization - let PDF.js normalize the text
|
|
59
|
+
// This matches how TextLayer will render it
|
|
60
|
+
const textContent = await page.getTextContent();
|
|
61
|
+
const strBuf = [];
|
|
62
|
+
for (const item of textContent.items) {
|
|
63
|
+
if ('str' in item) {
|
|
64
|
+
strBuf.push(item.str);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Join all text items directly (no separator)
|
|
68
|
+
// This creates a searchable string that matches the concatenation
|
|
69
|
+
// of all text spans in the rendered page
|
|
70
|
+
this.pageContents[i] = strBuf.join('');
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(`Unable to get text content for page ${i + 1}`, e);
|
|
74
|
+
this.pageContents[i] = '';
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
this.extractTextPromises[i] = promise;
|
|
78
|
+
}
|
|
79
|
+
await Promise.all(this.extractTextPromises);
|
|
80
|
+
}
|
|
81
|
+
async find(options) {
|
|
82
|
+
const { query, highlightAll = true, caseSensitive = false, entireWord = false, findPrevious = false } = options;
|
|
83
|
+
// Clear previous state
|
|
84
|
+
this.clearHighlights();
|
|
85
|
+
if (!query || query.trim() === '') {
|
|
86
|
+
this.query = '';
|
|
87
|
+
this.allMatches = [];
|
|
88
|
+
this.selectedMatchIndex = -1;
|
|
89
|
+
this.matchesCountTotal = 0;
|
|
90
|
+
this.eventBus.dispatch('updatefindmatchescount', {
|
|
91
|
+
matchesCount: { current: 0, total: 0 }
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.query = query;
|
|
96
|
+
this.highlightAll = highlightAll;
|
|
97
|
+
this.caseSensitive = caseSensitive;
|
|
98
|
+
this.entireWord = entireWord;
|
|
99
|
+
this.eventBus.dispatch('updatefindcontrolstate', {
|
|
100
|
+
state: FindState.PENDING
|
|
101
|
+
});
|
|
102
|
+
// Extract text from PDF if not already done
|
|
103
|
+
await this.extractText();
|
|
104
|
+
// Search all pages
|
|
105
|
+
this.pageMatches = [];
|
|
106
|
+
this.pageMatchesLength = [];
|
|
107
|
+
this.allMatches = [];
|
|
108
|
+
this.matchesCountTotal = 0;
|
|
109
|
+
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
|
110
|
+
for (let pageIndex = 0; pageIndex < this.pageContents.length; pageIndex++) {
|
|
111
|
+
const pageContent = this.pageContents[pageIndex];
|
|
112
|
+
const searchContent = caseSensitive ? pageContent : pageContent.toLowerCase();
|
|
113
|
+
const matches = [];
|
|
114
|
+
const matchesLength = [];
|
|
115
|
+
let pos = 0;
|
|
116
|
+
while ((pos = searchContent.indexOf(searchQuery, pos)) !== -1) {
|
|
117
|
+
// Check entire word if required
|
|
118
|
+
if (entireWord) {
|
|
119
|
+
const before = pos > 0 ? searchContent[pos - 1] : ' ';
|
|
120
|
+
const after = pos + searchQuery.length < searchContent.length
|
|
121
|
+
? searchContent[pos + searchQuery.length]
|
|
122
|
+
: ' ';
|
|
123
|
+
if (/\w/.test(before) || /\w/.test(after)) {
|
|
124
|
+
pos++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
matches.push(pos);
|
|
129
|
+
matchesLength.push(searchQuery.length);
|
|
130
|
+
this.allMatches.push({
|
|
131
|
+
pageIndex,
|
|
132
|
+
matchIndex: matches.length - 1
|
|
133
|
+
});
|
|
134
|
+
pos++;
|
|
135
|
+
}
|
|
136
|
+
this.pageMatches[pageIndex] = matches;
|
|
137
|
+
this.pageMatchesLength[pageIndex] = matchesLength;
|
|
138
|
+
this.matchesCountTotal += matches.length;
|
|
139
|
+
}
|
|
140
|
+
// Select first or last match based on direction
|
|
141
|
+
if (this.allMatches.length > 0) {
|
|
142
|
+
this.selectedMatchIndex = findPrevious ? this.allMatches.length - 1 : 0;
|
|
143
|
+
this.highlightAllPages();
|
|
144
|
+
this.scrollToSelectedMatch();
|
|
145
|
+
this.eventBus.dispatch('updatefindcontrolstate', {
|
|
146
|
+
state: FindState.FOUND
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.selectedMatchIndex = -1;
|
|
151
|
+
this.eventBus.dispatch('updatefindcontrolstate', {
|
|
152
|
+
state: FindState.NOT_FOUND
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
this.eventBus.dispatch('updatefindmatchescount', {
|
|
156
|
+
matchesCount: {
|
|
157
|
+
current: this.selectedMatchIndex + 1,
|
|
158
|
+
total: this.matchesCountTotal
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
findNext() {
|
|
163
|
+
if (this.allMatches.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
this.selectedMatchIndex = (this.selectedMatchIndex + 1) % this.allMatches.length;
|
|
166
|
+
this.highlightAllPages();
|
|
167
|
+
this.scrollToSelectedMatch();
|
|
168
|
+
const wrapped = this.selectedMatchIndex === 0;
|
|
169
|
+
this.eventBus.dispatch('updatefindcontrolstate', {
|
|
170
|
+
state: wrapped ? FindState.WRAPPED : FindState.FOUND
|
|
171
|
+
});
|
|
172
|
+
this.eventBus.dispatch('updatefindmatchescount', {
|
|
173
|
+
matchesCount: {
|
|
174
|
+
current: this.selectedMatchIndex + 1,
|
|
175
|
+
total: this.matchesCountTotal
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
findPrevious() {
|
|
180
|
+
if (this.allMatches.length === 0)
|
|
181
|
+
return;
|
|
182
|
+
this.selectedMatchIndex =
|
|
183
|
+
(this.selectedMatchIndex - 1 + this.allMatches.length) % this.allMatches.length;
|
|
184
|
+
this.highlightAllPages();
|
|
185
|
+
this.scrollToSelectedMatch();
|
|
186
|
+
const wrapped = this.selectedMatchIndex === this.allMatches.length - 1;
|
|
187
|
+
this.eventBus.dispatch('updatefindcontrolstate', {
|
|
188
|
+
state: wrapped ? FindState.WRAPPED : FindState.FOUND
|
|
189
|
+
});
|
|
190
|
+
this.eventBus.dispatch('updatefindmatchescount', {
|
|
191
|
+
matchesCount: {
|
|
192
|
+
current: this.selectedMatchIndex + 1,
|
|
193
|
+
total: this.matchesCountTotal
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
highlightAllPages() {
|
|
198
|
+
this.clearHighlights();
|
|
199
|
+
for (let pageIndex = 0; pageIndex < this.viewer.pagesCount; pageIndex++) {
|
|
200
|
+
this.highlightPage(pageIndex);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
highlightPage(pageIndex) {
|
|
204
|
+
const pageView = this.viewer.getPageView(pageIndex);
|
|
205
|
+
if (!pageView)
|
|
206
|
+
return;
|
|
207
|
+
const matches = this.pageMatches[pageIndex];
|
|
208
|
+
const matchesLength = this.pageMatchesLength[pageIndex];
|
|
209
|
+
if (!matches || matches.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
const textDivs = pageView.textDivs;
|
|
212
|
+
const textContentItemsStr = pageView.textContentItemsStr;
|
|
213
|
+
if (!textDivs || !textContentItemsStr || textContentItemsStr.length === 0)
|
|
214
|
+
return;
|
|
215
|
+
// Convert match positions to div positions
|
|
216
|
+
const convertedMatches = this.convertMatches(matches, matchesLength, textContentItemsStr);
|
|
217
|
+
// Find which match in allMatches corresponds to this page's selected match
|
|
218
|
+
let selectedMatchOnPage = -1;
|
|
219
|
+
if (this.selectedMatchIndex >= 0) {
|
|
220
|
+
const selectedGlobal = this.allMatches[this.selectedMatchIndex];
|
|
221
|
+
if (selectedGlobal.pageIndex === pageIndex) {
|
|
222
|
+
selectedMatchOnPage = selectedGlobal.matchIndex;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Render the matches
|
|
226
|
+
this.renderMatches(convertedMatches, textDivs, textContentItemsStr, selectedMatchOnPage, this.highlightAll);
|
|
227
|
+
}
|
|
228
|
+
convertMatches(matches, matchesLength, textContentItemsStr) {
|
|
229
|
+
if (!matches || matches.length === 0) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const result = [];
|
|
233
|
+
// Build cumulative text length array
|
|
234
|
+
const textLengths = [];
|
|
235
|
+
let totalLen = 0;
|
|
236
|
+
for (const str of textContentItemsStr) {
|
|
237
|
+
textLengths.push(totalLen);
|
|
238
|
+
totalLen += str.length;
|
|
239
|
+
}
|
|
240
|
+
for (let m = 0; m < matches.length; m++) {
|
|
241
|
+
const matchStart = matches[m];
|
|
242
|
+
const matchEnd = matchStart + matchesLength[m];
|
|
243
|
+
// Find the div containing the start of the match
|
|
244
|
+
let beginDivIdx = 0;
|
|
245
|
+
for (let i = 0; i < textLengths.length; i++) {
|
|
246
|
+
if (i === textLengths.length - 1 ||
|
|
247
|
+
(matchStart >= textLengths[i] &&
|
|
248
|
+
matchStart < textLengths[i] + textContentItemsStr[i].length)) {
|
|
249
|
+
beginDivIdx = i;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Find the div containing the end of the match
|
|
254
|
+
let endDivIdx = beginDivIdx;
|
|
255
|
+
for (let i = beginDivIdx; i < textLengths.length; i++) {
|
|
256
|
+
if (i === textLengths.length - 1 ||
|
|
257
|
+
matchEnd <= textLengths[i] + textContentItemsStr[i].length) {
|
|
258
|
+
endDivIdx = i;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
result.push({
|
|
263
|
+
begin: {
|
|
264
|
+
divIdx: beginDivIdx,
|
|
265
|
+
offset: matchStart - textLengths[beginDivIdx]
|
|
266
|
+
},
|
|
267
|
+
end: {
|
|
268
|
+
divIdx: endDivIdx,
|
|
269
|
+
offset: matchEnd - textLengths[endDivIdx]
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
renderMatches(matches, textDivs, textContentItemsStr, selectedMatchIdx, highlightAll) {
|
|
276
|
+
if (matches.length === 0)
|
|
277
|
+
return;
|
|
278
|
+
let prevEnd = null;
|
|
279
|
+
const infinity = { divIdx: -1, offset: undefined };
|
|
280
|
+
const beginText = (begin, className) => {
|
|
281
|
+
const divIdx = begin.divIdx;
|
|
282
|
+
textDivs[divIdx].textContent = '';
|
|
283
|
+
appendTextToDiv(divIdx, 0, begin.offset, className);
|
|
284
|
+
};
|
|
285
|
+
const appendTextToDiv = (divIdx, fromOffset, toOffset, className) => {
|
|
286
|
+
const div = textDivs[divIdx];
|
|
287
|
+
if (!div)
|
|
288
|
+
return;
|
|
289
|
+
const text = textContentItemsStr[divIdx];
|
|
290
|
+
const content = text.substring(fromOffset, toOffset);
|
|
291
|
+
if (!content)
|
|
292
|
+
return;
|
|
293
|
+
const node = document.createTextNode(content);
|
|
294
|
+
if (className) {
|
|
295
|
+
const span = document.createElement('span');
|
|
296
|
+
span.className = `${className} appended`;
|
|
297
|
+
span.appendChild(node);
|
|
298
|
+
div.appendChild(span);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
div.appendChild(node);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
// Determine range of matches to highlight
|
|
305
|
+
let i0 = selectedMatchIdx;
|
|
306
|
+
let i1 = i0 + 1;
|
|
307
|
+
if (highlightAll) {
|
|
308
|
+
i0 = 0;
|
|
309
|
+
i1 = matches.length;
|
|
310
|
+
}
|
|
311
|
+
else if (selectedMatchIdx < 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let lastDivIdx = -1;
|
|
315
|
+
let lastOffset = -1;
|
|
316
|
+
for (let i = i0; i < i1; i++) {
|
|
317
|
+
const match = matches[i];
|
|
318
|
+
const begin = match.begin;
|
|
319
|
+
// Skip duplicate matches at the same position (e.g., ligatures)
|
|
320
|
+
if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
lastDivIdx = begin.divIdx;
|
|
324
|
+
lastOffset = begin.offset;
|
|
325
|
+
const end = match.end;
|
|
326
|
+
const isSelected = i === selectedMatchIdx;
|
|
327
|
+
const highlightSuffix = isSelected ? ' selected' : '';
|
|
328
|
+
// Match inside new div
|
|
329
|
+
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
|
|
330
|
+
// If there was a previous div, add remaining text
|
|
331
|
+
if (prevEnd !== null) {
|
|
332
|
+
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
|
|
333
|
+
}
|
|
334
|
+
// Clear the div and set content until the match start
|
|
335
|
+
beginText(begin);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Same div as previous match - add text between matches
|
|
339
|
+
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
|
|
340
|
+
}
|
|
341
|
+
if (begin.divIdx === end.divIdx) {
|
|
342
|
+
// Single div match
|
|
343
|
+
appendTextToDiv(begin.divIdx, begin.offset, end.offset, 'highlight' + highlightSuffix);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// Multi-div match
|
|
347
|
+
// Highlight from begin to end of first div
|
|
348
|
+
appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, 'highlight begin' + highlightSuffix);
|
|
349
|
+
// Highlight entire middle divs
|
|
350
|
+
for (let n = begin.divIdx + 1; n < end.divIdx; n++) {
|
|
351
|
+
textDivs[n].className = 'highlight middle' + highlightSuffix;
|
|
352
|
+
}
|
|
353
|
+
// Start end div and highlight up to match end
|
|
354
|
+
beginText(end, 'highlight end' + highlightSuffix);
|
|
355
|
+
}
|
|
356
|
+
prevEnd = end;
|
|
357
|
+
}
|
|
358
|
+
// Add remaining text after last match
|
|
359
|
+
if (prevEnd) {
|
|
360
|
+
appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
scrollToSelectedMatch() {
|
|
364
|
+
if (this.selectedMatchIndex === -1)
|
|
365
|
+
return;
|
|
366
|
+
const match = this.allMatches[this.selectedMatchIndex];
|
|
367
|
+
if (!match)
|
|
368
|
+
return;
|
|
369
|
+
// First scroll to the page
|
|
370
|
+
this.viewer.scrollToPage(match.pageIndex + 1);
|
|
371
|
+
// Then scroll to the highlighted element only if it's not already visible
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
const selectedElement = this.viewer.container.querySelector('.highlight.selected');
|
|
374
|
+
if (selectedElement) {
|
|
375
|
+
const container = this.viewer.container;
|
|
376
|
+
const containerRect = container.getBoundingClientRect();
|
|
377
|
+
const elementRect = selectedElement.getBoundingClientRect();
|
|
378
|
+
// Check if element is visible within the container
|
|
379
|
+
const isVisible = elementRect.top >= containerRect.top &&
|
|
380
|
+
elementRect.bottom <= containerRect.bottom &&
|
|
381
|
+
elementRect.left >= containerRect.left &&
|
|
382
|
+
elementRect.right <= containerRect.right;
|
|
383
|
+
if (!isVisible) {
|
|
384
|
+
selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}, 100);
|
|
388
|
+
}
|
|
389
|
+
clearHighlights() {
|
|
390
|
+
// Clear all highlights from all pages
|
|
391
|
+
for (let pageIndex = 0; pageIndex < this.viewer.pagesCount; pageIndex++) {
|
|
392
|
+
const pageView = this.viewer.getPageView(pageIndex);
|
|
393
|
+
if (!pageView)
|
|
394
|
+
continue;
|
|
395
|
+
const textDivs = pageView.textDivs;
|
|
396
|
+
const textContentItemsStr = pageView.textContentItemsStr;
|
|
397
|
+
if (!textDivs || !textContentItemsStr)
|
|
398
|
+
continue;
|
|
399
|
+
for (let i = 0; i < textDivs.length; i++) {
|
|
400
|
+
const div = textDivs[i];
|
|
401
|
+
if (div) {
|
|
402
|
+
div.textContent = textContentItemsStr[i] || '';
|
|
403
|
+
div.className = '';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
get matchesCount() {
|
|
409
|
+
return {
|
|
410
|
+
current: this.selectedMatchIndex + 1,
|
|
411
|
+
total: this.matchesCountTotal
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
reset() {
|
|
415
|
+
this.clearHighlights();
|
|
416
|
+
this.query = '';
|
|
417
|
+
this.allMatches = [];
|
|
418
|
+
this.pageMatches = [];
|
|
419
|
+
this.pageMatchesLength = [];
|
|
420
|
+
this.selectedMatchIndex = -1;
|
|
421
|
+
this.matchesCountTotal = 0;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDFPageView - Renders a single PDF page with canvas and text layer.
|
|
3
|
+
* This is a derivative work based on PDF.js pdf_page_view.js
|
|
4
|
+
*/
|
|
5
|
+
import type { PDFPageProxy, PageViewport } from 'pdfjs-dist';
|
|
6
|
+
import type { EventBus } from './EventBus.js';
|
|
7
|
+
export interface PDFPageViewOptions {
|
|
8
|
+
container: HTMLElement;
|
|
9
|
+
id: number;
|
|
10
|
+
defaultViewport: PageViewport;
|
|
11
|
+
eventBus: EventBus;
|
|
12
|
+
scale?: number;
|
|
13
|
+
rotation?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const RenderingStates: {
|
|
16
|
+
readonly INITIAL: 0;
|
|
17
|
+
readonly RUNNING: 1;
|
|
18
|
+
readonly PAUSED: 2;
|
|
19
|
+
readonly FINISHED: 3;
|
|
20
|
+
};
|
|
21
|
+
export type RenderingState = (typeof RenderingStates)[keyof typeof RenderingStates];
|
|
22
|
+
export declare class PDFPageView {
|
|
23
|
+
readonly id: number;
|
|
24
|
+
readonly eventBus: EventBus;
|
|
25
|
+
private container;
|
|
26
|
+
private viewport;
|
|
27
|
+
private pdfPage;
|
|
28
|
+
private scale;
|
|
29
|
+
private rotation;
|
|
30
|
+
private pdfPageRotate;
|
|
31
|
+
div: HTMLDivElement;
|
|
32
|
+
private canvas;
|
|
33
|
+
private canvasWrapper;
|
|
34
|
+
private textLayerDiv;
|
|
35
|
+
private loadingDiv;
|
|
36
|
+
renderingState: RenderingState;
|
|
37
|
+
private renderTask;
|
|
38
|
+
private textLayer;
|
|
39
|
+
private textLayerRendered;
|
|
40
|
+
textDivs: HTMLElement[];
|
|
41
|
+
textContentItemsStr: string[];
|
|
42
|
+
constructor(options: PDFPageViewOptions);
|
|
43
|
+
private setDimensions;
|
|
44
|
+
setPdfPage(pdfPage: PDFPageProxy): void;
|
|
45
|
+
private updateViewport;
|
|
46
|
+
update({ scale, rotation }: {
|
|
47
|
+
scale?: number;
|
|
48
|
+
rotation?: number;
|
|
49
|
+
}): void;
|
|
50
|
+
private resetCanvas;
|
|
51
|
+
reset(): void;
|
|
52
|
+
draw(): Promise<void>;
|
|
53
|
+
private renderTextLayer;
|
|
54
|
+
cancelRendering(): void;
|
|
55
|
+
destroy(): void;
|
|
56
|
+
get width(): number;
|
|
57
|
+
get height(): number;
|
|
58
|
+
}
|