pretext-pdfjs 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 +21 -0
- package/README.md +118 -0
- package/package.json +42 -0
- package/src/index.js +67 -0
- package/src/measurement-cache.js +266 -0
- package/src/pinch.js +498 -0
- package/src/pretext-text-layer.js +484 -0
- package/src/viewer.js +213 -0
package/src/pinch.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pretext-pdf/pinch
|
|
3
|
+
*
|
|
4
|
+
* Integrates pinch-type (Lucas Crespo) into the PDF viewer pipeline.
|
|
5
|
+
* Enables three reading modes for PDF text:
|
|
6
|
+
*
|
|
7
|
+
* 1. pinchType — pinch-to-zoom resizes text, not the page
|
|
8
|
+
* 2. scrollMorph — fisheye: center text large/bright, edges small/dim
|
|
9
|
+
* 3. pinchMorph — both combined
|
|
10
|
+
*
|
|
11
|
+
* These modes extract text from a PDF page via pdfjs-dist, then render
|
|
12
|
+
* it to a Canvas using @chenglou/pretext for layout and pinch-type's
|
|
13
|
+
* gesture engine for interaction.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import { createPDFPinchReader } from "pretext-pdf/pinch";
|
|
17
|
+
*
|
|
18
|
+
* const reader = createPDFPinchReader(container, {
|
|
19
|
+
* mode: "pinchMorph",
|
|
20
|
+
* workerSrc: "path/to/pdf.worker.min.mjs",
|
|
21
|
+
* });
|
|
22
|
+
* await reader.open("document.pdf");
|
|
23
|
+
* await reader.showPage(1); // extracts text, renders with pinch-type
|
|
24
|
+
* reader.destroy();
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { prepareWithSegments, layoutWithLines } from "@chenglou/pretext";
|
|
28
|
+
|
|
29
|
+
// ─── Shared helpers ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function clamp(v, min, max) {
|
|
32
|
+
return Math.max(min, Math.min(max, v));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createCanvas(container) {
|
|
36
|
+
const canvas = document.createElement("canvas");
|
|
37
|
+
canvas.style.display = "block";
|
|
38
|
+
canvas.style.width = "100%";
|
|
39
|
+
canvas.style.height = "100%";
|
|
40
|
+
canvas.style.touchAction = "none";
|
|
41
|
+
container.appendChild(canvas);
|
|
42
|
+
return canvas;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pinchDist(e) {
|
|
46
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
47
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
48
|
+
return Math.hypot(dx, dy);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Core gesture+render engine ────────────────────────────────────────────
|
|
52
|
+
// Adapted from pinch-type by Lucas Crespo (MIT)
|
|
53
|
+
// Original: https://github.com/lucascrespo23/pinch-type
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {HTMLElement} container
|
|
57
|
+
* @param {Object} opts
|
|
58
|
+
* @param {"pinchType"|"scrollMorph"|"pinchMorph"} opts.mode
|
|
59
|
+
* @returns {Object} instance with setText, resize, destroy, canvas
|
|
60
|
+
*/
|
|
61
|
+
function createTextCanvas(container, opts = {}) {
|
|
62
|
+
const mode = opts.mode || "pinchType";
|
|
63
|
+
const minFont = opts.minFontSize ?? 8;
|
|
64
|
+
const maxFont = opts.maxFontSize ?? 60;
|
|
65
|
+
const fontFamily = opts.fontFamily ?? '"Inter", system-ui, -apple-system, sans-serif';
|
|
66
|
+
const lhRatio = opts.lineHeight ?? 1.57;
|
|
67
|
+
const padding = opts.padding ?? 28;
|
|
68
|
+
const bg = opts.background ?? "#0a0a0a";
|
|
69
|
+
const textColor = opts.textColor ?? "#e5e5e5";
|
|
70
|
+
const friction = opts.friction ?? 0.95;
|
|
71
|
+
const morphRadius = opts.morphRadius ?? 300;
|
|
72
|
+
const onZoom = opts.onZoom;
|
|
73
|
+
|
|
74
|
+
// Mutable state
|
|
75
|
+
let fontSize = opts.fontSize ?? 18;
|
|
76
|
+
let centerSize = opts.centerFontSize ?? 26;
|
|
77
|
+
let edgeSize = opts.edgeFontSize ?? 11;
|
|
78
|
+
const initialRatio = edgeSize / centerSize;
|
|
79
|
+
|
|
80
|
+
const canvas = createCanvas(container);
|
|
81
|
+
const ctx = canvas.getContext("2d");
|
|
82
|
+
let dpr = Math.min(devicePixelRatio || 1, 3);
|
|
83
|
+
let W = 0, H = 0;
|
|
84
|
+
let rawText = "";
|
|
85
|
+
let lines = [];
|
|
86
|
+
let totalHeight = 0, maxScroll = 0;
|
|
87
|
+
let scrollY = 0, scrollVelocity = 0;
|
|
88
|
+
let touchLastY = 0, touchLastTime = 0, isTouching = false;
|
|
89
|
+
let pinchActive = false, pinchStartDist = 0;
|
|
90
|
+
let pinchStartSize = 0, pinchStartCenter = 0, pinchStartEdge = 0;
|
|
91
|
+
let raf = 0, destroyed = false;
|
|
92
|
+
|
|
93
|
+
// ── Layout (uses Pretext) ──
|
|
94
|
+
|
|
95
|
+
function layout() {
|
|
96
|
+
if (!rawText || W === 0) return;
|
|
97
|
+
const maxW = W - padding * 2;
|
|
98
|
+
const fs = mode === "pinchType" ? fontSize : centerSize;
|
|
99
|
+
const lh = fs * lhRatio;
|
|
100
|
+
const font = `400 ${fs}px ${fontFamily}`;
|
|
101
|
+
const paragraphs = rawText.split("\n\n");
|
|
102
|
+
lines = [];
|
|
103
|
+
let curY = padding + 10;
|
|
104
|
+
for (const para of paragraphs) {
|
|
105
|
+
const trimmed = para.trim();
|
|
106
|
+
if (!trimmed) continue;
|
|
107
|
+
ctx.font = font;
|
|
108
|
+
const prepared = prepareWithSegments(trimmed, font);
|
|
109
|
+
const result = layoutWithLines(prepared, maxW, lh);
|
|
110
|
+
for (let li = 0; li < result.lines.length; li++) {
|
|
111
|
+
lines.push({
|
|
112
|
+
text: result.lines[li].text,
|
|
113
|
+
y: curY + li * lh,
|
|
114
|
+
baseSize: fs,
|
|
115
|
+
weight: 400,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
curY += result.lines.length * lh + lh * 0.6;
|
|
119
|
+
}
|
|
120
|
+
totalHeight = curY + padding;
|
|
121
|
+
maxScroll = Math.max(0, totalHeight - H);
|
|
122
|
+
scrollY = clamp(scrollY, 0, maxScroll);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Render ──
|
|
126
|
+
|
|
127
|
+
function render() {
|
|
128
|
+
const d = dpr;
|
|
129
|
+
ctx.fillStyle = bg;
|
|
130
|
+
ctx.fillRect(0, 0, W * d, H * d);
|
|
131
|
+
ctx.textBaseline = "top";
|
|
132
|
+
|
|
133
|
+
if (mode === "pinchType") {
|
|
134
|
+
// Uniform text
|
|
135
|
+
ctx.fillStyle = textColor;
|
|
136
|
+
ctx.font = `400 ${fontSize * d}px ${fontFamily}`;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const screenY = line.y - scrollY;
|
|
139
|
+
if (screenY < -100 || screenY > H + 100) continue;
|
|
140
|
+
ctx.fillText(line.text, padding * d, screenY * d);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// Morph (scrollMorph or pinchMorph)
|
|
144
|
+
const viewCenter = H / 2;
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const screenY = line.y - scrollY;
|
|
147
|
+
if (screenY < -100 || screenY > H + 100) continue;
|
|
148
|
+
const dist = Math.abs(screenY - viewCenter);
|
|
149
|
+
const t = Math.min(dist / morphRadius, 1);
|
|
150
|
+
const ease = 1 - (1 - t) ** 3;
|
|
151
|
+
const fs = centerSize + (edgeSize - centerSize) * ease;
|
|
152
|
+
const opacity = 1.0 + (0.25 - 1.0) * ease;
|
|
153
|
+
const c = Math.round(255 - (255 - 102) * ease);
|
|
154
|
+
ctx.save();
|
|
155
|
+
ctx.globalAlpha = opacity;
|
|
156
|
+
ctx.fillStyle = `rgb(${c},${c},${c})`;
|
|
157
|
+
ctx.font = `${line.weight} ${fs * d}px ${fontFamily}`;
|
|
158
|
+
const yOffset = (fs - line.baseSize) * 0.5;
|
|
159
|
+
ctx.fillText(line.text, padding * d, (screenY - yOffset) * d);
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Animation loop ──
|
|
166
|
+
|
|
167
|
+
function loop() {
|
|
168
|
+
if (destroyed) return;
|
|
169
|
+
if (!isTouching) {
|
|
170
|
+
scrollY += scrollVelocity;
|
|
171
|
+
scrollVelocity *= friction;
|
|
172
|
+
if (scrollY < 0) { scrollY *= 0.85; scrollVelocity *= 0.5; }
|
|
173
|
+
else if (scrollY > maxScroll) {
|
|
174
|
+
scrollY = maxScroll + (scrollY - maxScroll) * 0.85;
|
|
175
|
+
scrollVelocity *= 0.5;
|
|
176
|
+
}
|
|
177
|
+
if (Math.abs(scrollVelocity) < 0.1) scrollVelocity = 0;
|
|
178
|
+
}
|
|
179
|
+
render();
|
|
180
|
+
raf = requestAnimationFrame(loop);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Touch handlers ──
|
|
184
|
+
|
|
185
|
+
function onTouchStart(e) {
|
|
186
|
+
if (e.touches.length === 2 && mode !== "scrollMorph") {
|
|
187
|
+
pinchActive = true;
|
|
188
|
+
pinchStartDist = pinchDist(e);
|
|
189
|
+
pinchStartSize = fontSize;
|
|
190
|
+
pinchStartCenter = centerSize;
|
|
191
|
+
pinchStartEdge = edgeSize;
|
|
192
|
+
scrollVelocity = 0;
|
|
193
|
+
isTouching = false;
|
|
194
|
+
} else if (e.touches.length === 1 && !pinchActive) {
|
|
195
|
+
isTouching = true;
|
|
196
|
+
scrollVelocity = 0;
|
|
197
|
+
touchLastY = e.touches[0].clientY;
|
|
198
|
+
touchLastTime = performance.now();
|
|
199
|
+
}
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onTouchMove(e) {
|
|
204
|
+
if (pinchActive && e.touches.length === 2 && mode !== "scrollMorph") {
|
|
205
|
+
const scale = pinchDist(e) / pinchStartDist;
|
|
206
|
+
if (mode === "pinchType") {
|
|
207
|
+
const newSize = clamp(Math.round(pinchStartSize * scale), minFont, maxFont);
|
|
208
|
+
if (newSize !== fontSize) {
|
|
209
|
+
fontSize = newSize;
|
|
210
|
+
layout();
|
|
211
|
+
onZoom?.(fontSize);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// pinchMorph
|
|
215
|
+
const newCenter = clamp(Math.round(pinchStartCenter * scale), minFont, maxFont);
|
|
216
|
+
const newEdge = clamp(
|
|
217
|
+
Math.round(pinchStartEdge * scale),
|
|
218
|
+
Math.max(minFont, 6),
|
|
219
|
+
Math.round(maxFont * initialRatio)
|
|
220
|
+
);
|
|
221
|
+
if (newCenter !== centerSize || newEdge !== edgeSize) {
|
|
222
|
+
centerSize = newCenter;
|
|
223
|
+
edgeSize = newEdge;
|
|
224
|
+
layout();
|
|
225
|
+
onZoom?.(centerSize, edgeSize);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!isTouching || e.touches.length !== 1) return;
|
|
232
|
+
const y = e.touches[0].clientY;
|
|
233
|
+
const dy = touchLastY - y;
|
|
234
|
+
const now = performance.now();
|
|
235
|
+
const dt = now - touchLastTime;
|
|
236
|
+
scrollY += dy;
|
|
237
|
+
scrollY = clamp(scrollY, -50, maxScroll + 50);
|
|
238
|
+
if (dt > 0) scrollVelocity = (dy / dt) * 16;
|
|
239
|
+
touchLastY = y;
|
|
240
|
+
touchLastTime = now;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function onTouchEnd(e) {
|
|
245
|
+
if (e.touches.length < 2) pinchActive = false;
|
|
246
|
+
if (e.touches.length === 0) isTouching = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onWheel(e) {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
if ((e.ctrlKey || e.metaKey) && mode !== "scrollMorph") {
|
|
252
|
+
const delta = e.deltaY > 0 ? -1 : 1;
|
|
253
|
+
if (mode === "pinchType") {
|
|
254
|
+
const newSize = clamp(fontSize + delta, minFont, maxFont);
|
|
255
|
+
if (newSize !== fontSize) { fontSize = newSize; layout(); onZoom?.(fontSize); }
|
|
256
|
+
} else {
|
|
257
|
+
const newCenter = clamp(centerSize + delta, minFont, maxFont);
|
|
258
|
+
if (newCenter !== centerSize) {
|
|
259
|
+
centerSize = newCenter;
|
|
260
|
+
edgeSize = clamp(Math.round(centerSize * initialRatio), 4, centerSize);
|
|
261
|
+
layout();
|
|
262
|
+
onZoom?.(centerSize, edgeSize);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
scrollY += e.deltaY;
|
|
267
|
+
scrollY = clamp(scrollY, -50, maxScroll + 50);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleResize() {
|
|
272
|
+
dpr = Math.min(devicePixelRatio || 1, 3);
|
|
273
|
+
W = container.clientWidth;
|
|
274
|
+
H = container.clientHeight;
|
|
275
|
+
canvas.width = W * dpr;
|
|
276
|
+
canvas.height = H * dpr;
|
|
277
|
+
canvas.style.width = W + "px";
|
|
278
|
+
canvas.style.height = H + "px";
|
|
279
|
+
layout();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Bind ──
|
|
283
|
+
|
|
284
|
+
canvas.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
285
|
+
canvas.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
286
|
+
canvas.addEventListener("touchend", onTouchEnd);
|
|
287
|
+
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
288
|
+
window.addEventListener("resize", handleResize);
|
|
289
|
+
handleResize();
|
|
290
|
+
raf = requestAnimationFrame(loop);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
setText(text) {
|
|
294
|
+
rawText = text;
|
|
295
|
+
scrollY = 0;
|
|
296
|
+
scrollVelocity = 0;
|
|
297
|
+
layout();
|
|
298
|
+
},
|
|
299
|
+
resize: handleResize,
|
|
300
|
+
destroy() {
|
|
301
|
+
destroyed = true;
|
|
302
|
+
cancelAnimationFrame(raf);
|
|
303
|
+
canvas.removeEventListener("touchstart", onTouchStart);
|
|
304
|
+
canvas.removeEventListener("touchmove", onTouchMove);
|
|
305
|
+
canvas.removeEventListener("touchend", onTouchEnd);
|
|
306
|
+
canvas.removeEventListener("wheel", onWheel);
|
|
307
|
+
window.removeEventListener("resize", handleResize);
|
|
308
|
+
canvas.remove();
|
|
309
|
+
},
|
|
310
|
+
get canvas() { return canvas; },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── PDF integration ───────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create a pinch-type PDF reader.
|
|
318
|
+
*
|
|
319
|
+
* Loads a PDF with pdfjs-dist, extracts text per page, and renders it
|
|
320
|
+
* using pinch-type's Canvas engine with Pretext layout.
|
|
321
|
+
*
|
|
322
|
+
* @param {HTMLElement} container - DOM element (should have width/height)
|
|
323
|
+
* @param {Object} options
|
|
324
|
+
* @param {"pinchType"|"scrollMorph"|"pinchMorph"} [options.mode="pinchType"]
|
|
325
|
+
* @param {string} [options.workerSrc] - pdf.worker URL
|
|
326
|
+
* @param {number} [options.fontSize=18]
|
|
327
|
+
* @param {number} [options.centerFontSize=26]
|
|
328
|
+
* @param {number} [options.edgeFontSize=11]
|
|
329
|
+
* @param {number} [options.minFontSize=8]
|
|
330
|
+
* @param {number} [options.maxFontSize=60]
|
|
331
|
+
* @param {string} [options.fontFamily]
|
|
332
|
+
* @param {number} [options.lineHeight=1.57]
|
|
333
|
+
* @param {number} [options.padding=28]
|
|
334
|
+
* @param {string} [options.background="#0a0a0a"]
|
|
335
|
+
* @param {string} [options.textColor="#e5e5e5"]
|
|
336
|
+
* @param {number} [options.friction=0.95]
|
|
337
|
+
* @param {number} [options.morphRadius=300]
|
|
338
|
+
* @param {Function} [options.onZoom]
|
|
339
|
+
* @param {Function} [options.onPageLoad] - called with { pageNum, text, numPages }
|
|
340
|
+
*/
|
|
341
|
+
export function createPDFPinchReader(container, options = {}) {
|
|
342
|
+
let pdfjs = null;
|
|
343
|
+
let pdfDoc = null;
|
|
344
|
+
let textInstance = null;
|
|
345
|
+
let currentPage = 0;
|
|
346
|
+
|
|
347
|
+
const mode = options.mode || "pinchType";
|
|
348
|
+
|
|
349
|
+
async function ensurePdfjs() {
|
|
350
|
+
if (pdfjs) return;
|
|
351
|
+
pdfjs = await import("pdfjs-dist");
|
|
352
|
+
if (options.workerSrc) {
|
|
353
|
+
pdfjs.GlobalWorkerOptions.workerSrc = options.workerSrc;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Extract plain text from a PDF page.
|
|
359
|
+
* Joins text items with spaces, preserves paragraph breaks.
|
|
360
|
+
*/
|
|
361
|
+
async function extractPageText(pageNum) {
|
|
362
|
+
const page = await pdfDoc.getPage(pageNum);
|
|
363
|
+
const content = await page.getTextContent();
|
|
364
|
+
|
|
365
|
+
// Build text with paragraph detection
|
|
366
|
+
let result = "";
|
|
367
|
+
let lastY = null;
|
|
368
|
+
let lastFontSize = 12;
|
|
369
|
+
|
|
370
|
+
for (const item of content.items) {
|
|
371
|
+
if (!item.str) continue;
|
|
372
|
+
|
|
373
|
+
if (item.transform) {
|
|
374
|
+
const currentY = item.transform[5];
|
|
375
|
+
const fontHeight = Math.hypot(item.transform[2], item.transform[3]);
|
|
376
|
+
if (fontHeight > 0) lastFontSize = fontHeight;
|
|
377
|
+
|
|
378
|
+
if (lastY !== null) {
|
|
379
|
+
const gap = Math.abs(currentY - lastY);
|
|
380
|
+
if (gap > lastFontSize * 1.8) {
|
|
381
|
+
// Paragraph break
|
|
382
|
+
result += "\n\n";
|
|
383
|
+
} else if (gap > lastFontSize * 0.3) {
|
|
384
|
+
// Line break within paragraph — add space
|
|
385
|
+
if (!result.endsWith(" ") && !result.endsWith("\n")) {
|
|
386
|
+
result += " ";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
lastY = currentY;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
result += item.str;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return result.trim();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
/**
|
|
401
|
+
* Load a PDF document.
|
|
402
|
+
* @param {string|Uint8Array|ArrayBuffer} source
|
|
403
|
+
* @returns {Promise<{numPages: number}>}
|
|
404
|
+
*/
|
|
405
|
+
async open(source) {
|
|
406
|
+
await ensurePdfjs();
|
|
407
|
+
const loadParams =
|
|
408
|
+
source instanceof Uint8Array || source instanceof ArrayBuffer
|
|
409
|
+
? { data: source }
|
|
410
|
+
: typeof source === "string"
|
|
411
|
+
? { url: source }
|
|
412
|
+
: source;
|
|
413
|
+
pdfDoc = await pdfjs.getDocument(loadParams).promise;
|
|
414
|
+
return { numPages: pdfDoc.numPages };
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Extract text from a page and render it with pinch-type.
|
|
419
|
+
* @param {number} pageNum - 1-based
|
|
420
|
+
* @returns {Promise<{text: string, lineCount: number}>}
|
|
421
|
+
*/
|
|
422
|
+
async showPage(pageNum) {
|
|
423
|
+
if (!pdfDoc) throw new Error("Call open() first");
|
|
424
|
+
if (pageNum < 1 || pageNum > pdfDoc.numPages) {
|
|
425
|
+
throw new RangeError(`Page ${pageNum} out of range`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const text = await extractPageText(pageNum);
|
|
429
|
+
currentPage = pageNum;
|
|
430
|
+
|
|
431
|
+
// Create or reuse the text canvas
|
|
432
|
+
if (!textInstance) {
|
|
433
|
+
textInstance = createTextCanvas(container, { ...options, mode });
|
|
434
|
+
}
|
|
435
|
+
textInstance.setText(text);
|
|
436
|
+
|
|
437
|
+
options.onPageLoad?.({
|
|
438
|
+
pageNum,
|
|
439
|
+
text,
|
|
440
|
+
numPages: pdfDoc.numPages,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return { text };
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/** Show all pages concatenated. */
|
|
447
|
+
async showAll() {
|
|
448
|
+
if (!pdfDoc) throw new Error("Call open() first");
|
|
449
|
+
let allText = "";
|
|
450
|
+
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
|
451
|
+
const pageText = await extractPageText(i);
|
|
452
|
+
if (i > 1) allText += "\n\n";
|
|
453
|
+
allText += pageText;
|
|
454
|
+
}
|
|
455
|
+
if (!textInstance) {
|
|
456
|
+
textInstance = createTextCanvas(container, { ...options, mode });
|
|
457
|
+
}
|
|
458
|
+
textInstance.setText(allText);
|
|
459
|
+
currentPage = -1; // all pages
|
|
460
|
+
return { text: allText };
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/** Go to next page. */
|
|
464
|
+
async nextPage() {
|
|
465
|
+
if (pdfDoc && currentPage > 0 && currentPage < pdfDoc.numPages) {
|
|
466
|
+
return this.showPage(currentPage + 1);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
/** Go to previous page. */
|
|
471
|
+
async prevPage() {
|
|
472
|
+
if (pdfDoc && currentPage > 1) {
|
|
473
|
+
return this.showPage(currentPage - 1);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
/** Resize (auto-called on window resize). */
|
|
478
|
+
resize() {
|
|
479
|
+
textInstance?.resize();
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
/** Clean up everything. */
|
|
483
|
+
destroy() {
|
|
484
|
+
textInstance?.destroy();
|
|
485
|
+
textInstance = null;
|
|
486
|
+
pdfDoc?.destroy();
|
|
487
|
+
pdfDoc = null;
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
get currentPage() { return currentPage; },
|
|
491
|
+
get numPages() { return pdfDoc?.numPages || 0; },
|
|
492
|
+
get canvas() { return textInstance?.canvas || null; },
|
|
493
|
+
get mode() { return mode; },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Also export the standalone text canvas for non-PDF use
|
|
498
|
+
export { createTextCanvas };
|