pdfnova 1.0.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/README.md +336 -0
- package/dist/AnnotationReader-ADHPZ3V5.mjs +3 -0
- package/dist/AnnotationReader-ADHPZ3V5.mjs.map +1 -0
- package/dist/AnnotationReader-C5XLXGWS.cjs +12 -0
- package/dist/AnnotationReader-C5XLXGWS.cjs.map +1 -0
- package/dist/AnnotationWriter-N3Y4KTPI.cjs +12 -0
- package/dist/AnnotationWriter-N3Y4KTPI.cjs.map +1 -0
- package/dist/AnnotationWriter-XWY5N6AZ.mjs +3 -0
- package/dist/AnnotationWriter-XWY5N6AZ.mjs.map +1 -0
- package/dist/FormFiller-2O4MOQYV.cjs +12 -0
- package/dist/FormFiller-2O4MOQYV.cjs.map +1 -0
- package/dist/FormFiller-6DLWMRN5.mjs +3 -0
- package/dist/FormFiller-6DLWMRN5.mjs.map +1 -0
- package/dist/FormFlattener-5MWLLH7W.cjs +12 -0
- package/dist/FormFlattener-5MWLLH7W.cjs.map +1 -0
- package/dist/FormFlattener-YQRQ3QOY.mjs +3 -0
- package/dist/FormFlattener-YQRQ3QOY.mjs.map +1 -0
- package/dist/FormReader-QAEDFD77.cjs +13 -0
- package/dist/FormReader-QAEDFD77.cjs.map +1 -0
- package/dist/FormReader-XEF3T5LD.mjs +4 -0
- package/dist/FormReader-XEF3T5LD.mjs.map +1 -0
- package/dist/SignatureVerifier-2IR7UQGU.mjs +3 -0
- package/dist/SignatureVerifier-2IR7UQGU.mjs.map +1 -0
- package/dist/SignatureVerifier-4KWQA7X6.cjs +12 -0
- package/dist/SignatureVerifier-4KWQA7X6.cjs.map +1 -0
- package/dist/WasmMock-2I3GVRQF.mjs +397 -0
- package/dist/WasmMock-2I3GVRQF.mjs.map +1 -0
- package/dist/WasmMock-OYPV4J6B.cjs +399 -0
- package/dist/WasmMock-OYPV4J6B.cjs.map +1 -0
- package/dist/chunk-2OWW5BYD.mjs +1847 -0
- package/dist/chunk-2OWW5BYD.mjs.map +1 -0
- package/dist/chunk-2YFCKMVK.cjs +72 -0
- package/dist/chunk-2YFCKMVK.cjs.map +1 -0
- package/dist/chunk-7FGOUG4Z.cjs +51 -0
- package/dist/chunk-7FGOUG4Z.cjs.map +1 -0
- package/dist/chunk-CPMUWWNS.cjs +44 -0
- package/dist/chunk-CPMUWWNS.cjs.map +1 -0
- package/dist/chunk-DVMAQ62T.cjs +109 -0
- package/dist/chunk-DVMAQ62T.cjs.map +1 -0
- package/dist/chunk-ETSUOY4U.cjs +104 -0
- package/dist/chunk-ETSUOY4U.cjs.map +1 -0
- package/dist/chunk-FD5RTJ5W.mjs +60 -0
- package/dist/chunk-FD5RTJ5W.mjs.map +1 -0
- package/dist/chunk-FO3DQLVB.mjs +42 -0
- package/dist/chunk-FO3DQLVB.mjs.map +1 -0
- package/dist/chunk-G2FA6VKV.cjs +62 -0
- package/dist/chunk-G2FA6VKV.cjs.map +1 -0
- package/dist/chunk-G7575D5X.mjs +67 -0
- package/dist/chunk-G7575D5X.mjs.map +1 -0
- package/dist/chunk-I7OBHZLB.mjs +75 -0
- package/dist/chunk-I7OBHZLB.mjs.map +1 -0
- package/dist/chunk-INA3KRJK.cjs +1863 -0
- package/dist/chunk-INA3KRJK.cjs.map +1 -0
- package/dist/chunk-RZFKZ2CA.cjs +77 -0
- package/dist/chunk-RZFKZ2CA.cjs.map +1 -0
- package/dist/chunk-VRJQZOCH.mjs +107 -0
- package/dist/chunk-VRJQZOCH.mjs.map +1 -0
- package/dist/chunk-X53667JS.mjs +102 -0
- package/dist/chunk-X53667JS.mjs.map +1 -0
- package/dist/chunk-XEHKVFPJ.mjs +49 -0
- package/dist/chunk-XEHKVFPJ.mjs.map +1 -0
- package/dist/index.cjs +112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.mjs +14 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lite-CMz-Yosm.d.cts +662 -0
- package/dist/lite-CMz-Yosm.d.ts +662 -0
- package/dist/lite.cjs +70 -0
- package/dist/lite.cjs.map +1 -0
- package/dist/lite.d.cts +1 -0
- package/dist/lite.d.ts +1 -0
- package/dist/lite.mjs +8 -0
- package/dist/lite.mjs.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1847 @@
|
|
|
1
|
+
import { RENDER_FLAG } from './chunk-G7575D5X.mjs';
|
|
2
|
+
|
|
3
|
+
// src/core/WasmLoader.ts
|
|
4
|
+
var PDFIUM_WASM_CDN = "https://cdn.jsdelivr.net/npm/@embedpdf/pdfium@2/dist/pdfium.wasm";
|
|
5
|
+
var IDB_NAME = "pdfnova-cache";
|
|
6
|
+
var IDB_STORE = "wasm";
|
|
7
|
+
var IDB_KEY = "pdfium-binary";
|
|
8
|
+
var wasmInstance = null;
|
|
9
|
+
var initPromise = null;
|
|
10
|
+
var forceMock = false;
|
|
11
|
+
var WasmLoader = class _WasmLoader {
|
|
12
|
+
static async load(options) {
|
|
13
|
+
if (wasmInstance) return wasmInstance;
|
|
14
|
+
if (initPromise) return initPromise;
|
|
15
|
+
initPromise = _WasmLoader._doLoad(options);
|
|
16
|
+
wasmInstance = await initPromise;
|
|
17
|
+
return wasmInstance;
|
|
18
|
+
}
|
|
19
|
+
static isLoaded() {
|
|
20
|
+
return wasmInstance !== null;
|
|
21
|
+
}
|
|
22
|
+
static getInstance() {
|
|
23
|
+
return wasmInstance;
|
|
24
|
+
}
|
|
25
|
+
static reset() {
|
|
26
|
+
if (wasmInstance) {
|
|
27
|
+
try {
|
|
28
|
+
wasmInstance._FPDF_DestroyLibrary();
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
wasmInstance = null;
|
|
33
|
+
initPromise = null;
|
|
34
|
+
}
|
|
35
|
+
/** Force mock mode (for unit tests in non-browser environments). */
|
|
36
|
+
static enableMock() {
|
|
37
|
+
forceMock = true;
|
|
38
|
+
}
|
|
39
|
+
static disableMock() {
|
|
40
|
+
forceMock = false;
|
|
41
|
+
}
|
|
42
|
+
/** Clear the cached WASM binary from IndexedDB. */
|
|
43
|
+
static async clearCache() {
|
|
44
|
+
try {
|
|
45
|
+
const db = await _WasmLoader._openIDB();
|
|
46
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
47
|
+
tx.objectStore(IDB_STORE).delete(IDB_KEY);
|
|
48
|
+
await new Promise((res, rej) => {
|
|
49
|
+
tx.oncomplete = () => res();
|
|
50
|
+
tx.onerror = () => rej(tx.error);
|
|
51
|
+
});
|
|
52
|
+
db.close();
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
static async _doLoad(options) {
|
|
57
|
+
if (forceMock || !_WasmLoader._canLoadWasm()) {
|
|
58
|
+
const { createMockModule } = await import('./WasmMock-2I3GVRQF.mjs');
|
|
59
|
+
return createMockModule();
|
|
60
|
+
}
|
|
61
|
+
return _WasmLoader._loadRealPdfium(options);
|
|
62
|
+
}
|
|
63
|
+
static _canLoadWasm() {
|
|
64
|
+
return typeof globalThis.WebAssembly !== "undefined" && typeof globalThis.fetch === "function" && typeof globalThis.document !== "undefined";
|
|
65
|
+
}
|
|
66
|
+
static async _loadRealPdfium(options) {
|
|
67
|
+
const wasmUrl = options?.wasmUrl ?? PDFIUM_WASM_CDN;
|
|
68
|
+
const { init } = await import('@embedpdf/pdfium');
|
|
69
|
+
let wasmBinary = await _WasmLoader._loadFromCache();
|
|
70
|
+
if (!wasmBinary) {
|
|
71
|
+
const response = await fetch(wasmUrl);
|
|
72
|
+
if (!response.ok) throw new Error(`Failed to fetch PDFium WASM: ${response.status}`);
|
|
73
|
+
wasmBinary = await response.arrayBuffer();
|
|
74
|
+
_WasmLoader._saveToCache(wasmBinary).catch(() => {
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const wrapped = await init({ wasmBinary });
|
|
78
|
+
wrapped.PDFiumExt_Init();
|
|
79
|
+
return _WasmLoader._adaptModule(wrapped);
|
|
80
|
+
}
|
|
81
|
+
// ─── IndexedDB WASM cache ────────────────────────────────────
|
|
82
|
+
static async _openIDB() {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const req = indexedDB.open(IDB_NAME, 1);
|
|
85
|
+
req.onupgradeneeded = () => {
|
|
86
|
+
req.result.createObjectStore(IDB_STORE);
|
|
87
|
+
};
|
|
88
|
+
req.onsuccess = () => resolve(req.result);
|
|
89
|
+
req.onerror = () => reject(req.error);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
static async _loadFromCache() {
|
|
93
|
+
try {
|
|
94
|
+
if (typeof indexedDB === "undefined") return null;
|
|
95
|
+
const db = await _WasmLoader._openIDB();
|
|
96
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
97
|
+
const req = tx.objectStore(IDB_STORE).get(IDB_KEY);
|
|
98
|
+
const result = await new Promise((res, rej) => {
|
|
99
|
+
req.onsuccess = () => res(req.result ?? null);
|
|
100
|
+
req.onerror = () => rej(req.error);
|
|
101
|
+
});
|
|
102
|
+
db.close();
|
|
103
|
+
return result;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
static async _saveToCache(binary) {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof indexedDB === "undefined") return;
|
|
111
|
+
const db = await _WasmLoader._openIDB();
|
|
112
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
113
|
+
tx.objectStore(IDB_STORE).put(binary, IDB_KEY);
|
|
114
|
+
await new Promise((res, rej) => {
|
|
115
|
+
tx.oncomplete = () => res();
|
|
116
|
+
tx.onerror = () => rej(tx.error);
|
|
117
|
+
});
|
|
118
|
+
db.close();
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Adapts an @embedpdf/pdfium WrappedPdfiumModule to our WasmModule interface.
|
|
124
|
+
* Uses cwrap with all-number signatures for raw C-level pointer-based access,
|
|
125
|
+
* matching the interface that WasmBridge and all downstream code expects.
|
|
126
|
+
*/
|
|
127
|
+
static _adaptModule(wrapped) {
|
|
128
|
+
const raw = wrapped.pdfium;
|
|
129
|
+
const cw = (name, ret, args) => raw.cwrap(name, ret, args);
|
|
130
|
+
const module = {
|
|
131
|
+
get HEAPU8() {
|
|
132
|
+
return raw.HEAPU8;
|
|
133
|
+
},
|
|
134
|
+
get HEAPU32() {
|
|
135
|
+
return raw.HEAPU32;
|
|
136
|
+
},
|
|
137
|
+
get HEAP32() {
|
|
138
|
+
return raw.HEAP32;
|
|
139
|
+
},
|
|
140
|
+
get HEAPF32() {
|
|
141
|
+
return raw.HEAPF32;
|
|
142
|
+
},
|
|
143
|
+
get HEAPF64() {
|
|
144
|
+
return raw.HEAPF64;
|
|
145
|
+
},
|
|
146
|
+
_malloc: raw.wasmExports.malloc,
|
|
147
|
+
_free: raw.wasmExports.free,
|
|
148
|
+
// PDFiumExt_Init already called — this is a no-op
|
|
149
|
+
_FPDF_InitLibraryWithConfig: () => {
|
|
150
|
+
},
|
|
151
|
+
_FPDF_DestroyLibrary: cw("FPDF_DestroyLibrary", null, []),
|
|
152
|
+
_FPDF_LoadMemDocument: cw("FPDF_LoadMemDocument", "number", ["number", "number", "number"]),
|
|
153
|
+
_FPDF_CloseDocument: cw("FPDF_CloseDocument", null, ["number"]),
|
|
154
|
+
_FPDF_GetLastError: cw("FPDF_GetLastError", "number", []),
|
|
155
|
+
_FPDF_GetPageCount: cw("FPDF_GetPageCount", "number", ["number"]),
|
|
156
|
+
_FPDF_LoadPage: cw("FPDF_LoadPage", "number", ["number", "number"]),
|
|
157
|
+
_FPDF_ClosePage: cw("FPDF_ClosePage", null, ["number"]),
|
|
158
|
+
_FPDF_GetPageWidthF: cw("FPDF_GetPageWidthF", "number", ["number"]),
|
|
159
|
+
_FPDF_GetPageHeightF: cw("FPDF_GetPageHeightF", "number", ["number"]),
|
|
160
|
+
_FPDFBitmap_Create: cw("FPDFBitmap_Create", "number", ["number", "number", "number"]),
|
|
161
|
+
_FPDFBitmap_FillRect: cw("FPDFBitmap_FillRect", null, ["number", "number", "number", "number", "number", "number"]),
|
|
162
|
+
_FPDFBitmap_GetBuffer: cw("FPDFBitmap_GetBuffer", "number", ["number"]),
|
|
163
|
+
_FPDFBitmap_GetStride: cw("FPDFBitmap_GetStride", "number", ["number"]),
|
|
164
|
+
_FPDFBitmap_Destroy: cw("FPDFBitmap_Destroy", null, ["number"]),
|
|
165
|
+
_FPDF_RenderPageBitmap: cw("FPDF_RenderPageBitmap", null, ["number", "number", "number", "number", "number", "number", "number", "number"]),
|
|
166
|
+
_FPDFText_LoadPage: cw("FPDFText_LoadPage", "number", ["number"]),
|
|
167
|
+
_FPDFText_ClosePage: cw("FPDFText_ClosePage", null, ["number"]),
|
|
168
|
+
_FPDFText_CountChars: cw("FPDFText_CountChars", "number", ["number"]),
|
|
169
|
+
_FPDFText_GetUnicode: cw("FPDFText_GetUnicode", "number", ["number", "number"]),
|
|
170
|
+
_FPDFText_GetFontSize: cw("FPDFText_GetFontSize", "number", ["number", "number"]),
|
|
171
|
+
_FPDFText_GetCharBox: cw("FPDFText_GetCharBox", "number", ["number", "number", "number", "number", "number", "number"]),
|
|
172
|
+
_FPDFText_GetText: cw("FPDFText_GetText", "number", ["number", "number", "number", "number"]),
|
|
173
|
+
_FPDFText_FindStart: cw("FPDFText_FindStart", "number", ["number", "number", "number", "number"]),
|
|
174
|
+
_FPDFText_FindNext: cw("FPDFText_FindNext", "number", ["number"]),
|
|
175
|
+
_FPDFText_FindClose: cw("FPDFText_FindClose", null, ["number"]),
|
|
176
|
+
_FPDFText_GetSchResultIndex: cw("FPDFText_GetSchResultIndex", "number", ["number"]),
|
|
177
|
+
_FPDFText_GetSchCount: cw("FPDFText_GetSchCount", "number", ["number"]),
|
|
178
|
+
_FPDFText_CountRects: cw("FPDFText_CountRects", "number", ["number", "number", "number"]),
|
|
179
|
+
_FPDFText_GetRect: cw("FPDFText_GetRect", "number", ["number", "number", "number", "number", "number", "number"]),
|
|
180
|
+
_FPDF_GetMetaText: cw("FPDF_GetMetaText", "number", ["number", "number", "number", "number"]),
|
|
181
|
+
_FPDFBookmark_GetFirstChild: cw("FPDFBookmark_GetFirstChild", "number", ["number", "number"]),
|
|
182
|
+
_FPDFBookmark_GetNextSibling: cw("FPDFBookmark_GetNextSibling", "number", ["number", "number"]),
|
|
183
|
+
_FPDFBookmark_GetTitle: cw("FPDFBookmark_GetTitle", "number", ["number", "number", "number"]),
|
|
184
|
+
_FPDFBookmark_GetAction: cw("FPDFBookmark_GetAction", "number", ["number"]),
|
|
185
|
+
_FPDFBookmark_GetDest: cw("FPDFBookmark_GetDest", "number", ["number", "number"]),
|
|
186
|
+
_FPDFDest_GetDestPageIndex: cw("FPDFDest_GetDestPageIndex", "number", ["number", "number"]),
|
|
187
|
+
_FPDFLink_LoadWebLinks: cw("FPDFLink_LoadWebLinks", "number", ["number"]),
|
|
188
|
+
_FPDFLink_CountWebLinks: cw("FPDFLink_CountWebLinks", "number", ["number"]),
|
|
189
|
+
_FPDFLink_GetURL: cw("FPDFLink_GetURL", "number", ["number", "number", "number", "number"]),
|
|
190
|
+
_FPDFLink_CountRects: cw("FPDFLink_CountRects", "number", ["number", "number"]),
|
|
191
|
+
_FPDFLink_GetRect: cw("FPDFLink_GetRect", "number", ["number", "number", "number", "number", "number", "number", "number"]),
|
|
192
|
+
_FPDFLink_CloseWebLinks: cw("FPDFLink_CloseWebLinks", null, ["number"]),
|
|
193
|
+
// Full-tier: annotations
|
|
194
|
+
_FPDFPage_GetAnnotCount: cw("FPDFPage_GetAnnotCount", "number", ["number"]),
|
|
195
|
+
_FPDFPage_GetAnnot: cw("FPDFPage_GetAnnot", "number", ["number", "number"]),
|
|
196
|
+
_FPDFPage_CreateAnnot: cw("FPDFPage_CreateAnnot", "number", ["number", "number"]),
|
|
197
|
+
_FPDFPage_RemoveAnnot: cw("FPDFPage_RemoveAnnot", "number", ["number", "number"]),
|
|
198
|
+
_FPDFAnnot_GetSubtype: cw("FPDFAnnot_GetSubtype", "number", ["number"]),
|
|
199
|
+
_FPDFAnnot_GetRect: cw("FPDFAnnot_GetRect", "number", ["number", "number"]),
|
|
200
|
+
_FPDFAnnot_SetRect: cw("FPDFAnnot_SetRect", "number", ["number", "number"]),
|
|
201
|
+
_FPDFAnnot_GetColor: cw("FPDFAnnot_GetColor", "number", ["number", "number", "number", "number", "number", "number"]),
|
|
202
|
+
_FPDFAnnot_SetColor: cw("FPDFAnnot_SetColor", "number", ["number", "number", "number", "number", "number", "number"]),
|
|
203
|
+
_FPDFAnnot_GetStringValue: cw("FPDFAnnot_GetStringValue", "number", ["number", "number", "number", "number"]),
|
|
204
|
+
_FPDFAnnot_SetStringValue: cw("FPDFAnnot_SetStringValue", "number", ["number", "number", "number"]),
|
|
205
|
+
_FPDFAnnot_AppendAttachmentPoints: cw("FPDFAnnot_AppendAttachmentPoints", "number", ["number", "number"]),
|
|
206
|
+
_FPDFAnnot_CountAttachmentPoints: cw("FPDFAnnot_CountAttachmentPoints", "number", ["number"]),
|
|
207
|
+
_FPDFAnnot_GetAttachmentPoints: cw("FPDFAnnot_GetAttachmentPoints", "number", ["number", "number", "number"]),
|
|
208
|
+
_FPDFPage_CloseAnnot: cw("FPDFPage_CloseAnnot", null, ["number"]),
|
|
209
|
+
// Full-tier: forms
|
|
210
|
+
_FPDFDOC_InitFormFillEnvironment: cw("FPDFDOC_InitFormFillEnvironment", "number", ["number", "number"]),
|
|
211
|
+
_FPDFDOC_ExitFormFillEnvironment: cw("FPDFDOC_ExitFormFillEnvironment", null, ["number"]),
|
|
212
|
+
_FPDF_GetFormType: cw("FPDF_GetFormType", "number", ["number"]),
|
|
213
|
+
_FPDFPage_HasFormFieldAtPoint: cw("FPDFPage_HasFormFieldAtPoint", "number", ["number", "number", "number", "number"]),
|
|
214
|
+
_FORM_OnMouseMove: cw("FORM_OnMouseMove", "number", ["number", "number", "number", "number", "number"]),
|
|
215
|
+
_FPDFAnnot_GetFormFieldType: cw("FPDFAnnot_GetFormFieldType", "number", ["number", "number"]),
|
|
216
|
+
_FPDFAnnot_GetFormFieldName: cw("FPDFAnnot_GetFormFieldName", "number", ["number", "number", "number", "number"]),
|
|
217
|
+
_FPDFAnnot_GetFormFieldValue: cw("FPDFAnnot_GetFormFieldValue", "number", ["number", "number", "number", "number"]),
|
|
218
|
+
_FPDFAnnot_SetFormFieldValue: cw("FPDFAnnot_SetFormFieldValue", null, ["number", "number", "number"]),
|
|
219
|
+
_FPDFAnnot_GetFormFieldFlags: cw("FPDFAnnot_GetFormFieldFlags", "number", ["number", "number"]),
|
|
220
|
+
_FPDFAnnot_IsChecked: cw("FPDFAnnot_IsChecked", "number", ["number", "number"]),
|
|
221
|
+
_FPDFPage_Flatten: cw("FPDFPage_Flatten", "number", ["number", "number"]),
|
|
222
|
+
_FPDF_SaveAsCopy: cw("FPDF_SaveAsCopy", "number", ["number", "number", "number"]),
|
|
223
|
+
_FPDF_SaveWithVersion: cw("FPDF_SaveWithVersion", "number", ["number", "number", "number", "number"]),
|
|
224
|
+
// Full-tier: signatures (note: real PDFium uses FPDFSignatureObj, our interface uses FPDFSignObj)
|
|
225
|
+
_FPDF_GetSignatureCount: cw("FPDF_GetSignatureCount", "number", ["number"]),
|
|
226
|
+
_FPDF_GetSignatureObject: cw("FPDF_GetSignatureObject", "number", ["number", "number"]),
|
|
227
|
+
_FPDFSignObj_GetContents: cw("FPDFSignatureObj_GetContents", "number", ["number", "number", "number"]),
|
|
228
|
+
_FPDFSignObj_GetByteRange: cw("FPDFSignatureObj_GetByteRange", "number", ["number", "number", "number"]),
|
|
229
|
+
_FPDFSignObj_GetSubFilter: cw("FPDFSignatureObj_GetSubFilter", "number", ["number", "number", "number"]),
|
|
230
|
+
_FPDFSignObj_GetReason: cw("FPDFSignatureObj_GetReason", "number", ["number", "number", "number"]),
|
|
231
|
+
_FPDFSignObj_GetTime: cw("FPDFSignatureObj_GetTime", "number", ["number", "number", "number"]),
|
|
232
|
+
_FPDFSignObj_GetDocMDPPermission: cw("FPDFSignatureObj_GetDocMDPPermission", "number", ["number"]),
|
|
233
|
+
// Emscripten utilities
|
|
234
|
+
UTF8ToString: raw.UTF8ToString.bind(raw),
|
|
235
|
+
UTF16ToString: raw.UTF16ToString.bind(raw),
|
|
236
|
+
stringToUTF8: raw.stringToUTF8.bind(raw),
|
|
237
|
+
stringToUTF16: raw.stringToUTF16.bind(raw),
|
|
238
|
+
lengthBytesUTF8: (str) => new TextEncoder().encode(str).length,
|
|
239
|
+
lengthBytesUTF16: (str) => str.length * 2
|
|
240
|
+
};
|
|
241
|
+
return module;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/text/TextExtractor.ts
|
|
246
|
+
var TextExtractor = class {
|
|
247
|
+
wasm;
|
|
248
|
+
bridge;
|
|
249
|
+
constructor(wasm, bridge) {
|
|
250
|
+
this.wasm = wasm;
|
|
251
|
+
this.bridge = bridge;
|
|
252
|
+
}
|
|
253
|
+
extractText(textPagePtr) {
|
|
254
|
+
const charCount = this.wasm._FPDFText_CountChars(textPagePtr);
|
|
255
|
+
if (charCount <= 0) return "";
|
|
256
|
+
const bufSize = (charCount + 1) * 2;
|
|
257
|
+
const buf = this.wasm._malloc(bufSize);
|
|
258
|
+
this.wasm._FPDFText_GetText(textPagePtr, 0, charCount, buf);
|
|
259
|
+
const text = this.bridge.readUTF16String(buf);
|
|
260
|
+
this.wasm._free(buf);
|
|
261
|
+
return text;
|
|
262
|
+
}
|
|
263
|
+
extractCharBoxes(textPagePtr) {
|
|
264
|
+
const charCount = this.wasm._FPDFText_CountChars(textPagePtr);
|
|
265
|
+
if (charCount <= 0) return [];
|
|
266
|
+
const leftPtr = this.bridge.allocateF64();
|
|
267
|
+
const rightPtr = this.bridge.allocateF64();
|
|
268
|
+
const bottomPtr = this.bridge.allocateF64();
|
|
269
|
+
const topPtr = this.bridge.allocateF64();
|
|
270
|
+
const boxes = [];
|
|
271
|
+
try {
|
|
272
|
+
for (let i = 0; i < charCount; i++) {
|
|
273
|
+
const unicode = this.wasm._FPDFText_GetUnicode(textPagePtr, i);
|
|
274
|
+
if (unicode === 0) continue;
|
|
275
|
+
const result = this.wasm._FPDFText_GetCharBox(
|
|
276
|
+
textPagePtr,
|
|
277
|
+
i,
|
|
278
|
+
leftPtr,
|
|
279
|
+
rightPtr,
|
|
280
|
+
bottomPtr,
|
|
281
|
+
topPtr
|
|
282
|
+
);
|
|
283
|
+
if (result) {
|
|
284
|
+
boxes.push({
|
|
285
|
+
char: String.fromCodePoint(unicode),
|
|
286
|
+
charCode: unicode,
|
|
287
|
+
left: this.bridge.readF64(leftPtr),
|
|
288
|
+
right: this.bridge.readF64(rightPtr),
|
|
289
|
+
bottom: this.bridge.readF64(bottomPtr),
|
|
290
|
+
top: this.bridge.readF64(topPtr),
|
|
291
|
+
fontSize: this.wasm._FPDFText_GetFontSize(textPagePtr, i),
|
|
292
|
+
index: i
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
this.bridge.freeAll(leftPtr, rightPtr, bottomPtr, topPtr);
|
|
298
|
+
}
|
|
299
|
+
return boxes;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Groups consecutive characters into text spans based on spatial proximity
|
|
303
|
+
* and font size. Spans represent logical "words" or "runs" of text.
|
|
304
|
+
*/
|
|
305
|
+
extractSpans(textPagePtr) {
|
|
306
|
+
const boxes = this.extractCharBoxes(textPagePtr);
|
|
307
|
+
if (boxes.length === 0) return [];
|
|
308
|
+
const spans = [];
|
|
309
|
+
let currentSpan = null;
|
|
310
|
+
for (const box of boxes) {
|
|
311
|
+
const isWhitespace = /\s/.test(box.char);
|
|
312
|
+
if (!currentSpan || isWhitespace || !this._isContinuation(currentSpan, box)) {
|
|
313
|
+
if (currentSpan && currentSpan.text.trim()) {
|
|
314
|
+
spans.push(currentSpan);
|
|
315
|
+
}
|
|
316
|
+
if (isWhitespace) {
|
|
317
|
+
currentSpan = null;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
currentSpan = {
|
|
321
|
+
text: box.char,
|
|
322
|
+
x: box.left,
|
|
323
|
+
y: box.bottom,
|
|
324
|
+
width: box.right - box.left,
|
|
325
|
+
height: box.top - box.bottom,
|
|
326
|
+
fontSize: box.fontSize,
|
|
327
|
+
charIndex: box.index,
|
|
328
|
+
charCount: 1
|
|
329
|
+
};
|
|
330
|
+
} else {
|
|
331
|
+
currentSpan.text += box.char;
|
|
332
|
+
currentSpan.width = box.right - currentSpan.x;
|
|
333
|
+
currentSpan.height = Math.max(currentSpan.height, box.top - box.bottom);
|
|
334
|
+
currentSpan.charCount++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (currentSpan && currentSpan.text.trim()) {
|
|
338
|
+
spans.push(currentSpan);
|
|
339
|
+
}
|
|
340
|
+
return spans;
|
|
341
|
+
}
|
|
342
|
+
_isContinuation(span, box) {
|
|
343
|
+
const sameBaseline = Math.abs(span.y - box.bottom) < span.fontSize * 0.5;
|
|
344
|
+
const sameFontSize = Math.abs(span.fontSize - box.fontSize) < 0.5;
|
|
345
|
+
const horizontalGap = box.left - (span.x + span.width);
|
|
346
|
+
const maxGap = span.fontSize * 0.5;
|
|
347
|
+
return sameBaseline && sameFontSize && horizontalGap < maxGap && horizontalGap > -span.fontSize * 0.3;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/text/TextLayerBuilder.ts
|
|
352
|
+
var TextLayerBuilder = class {
|
|
353
|
+
wasm;
|
|
354
|
+
bridge;
|
|
355
|
+
constructor(wasm, bridge) {
|
|
356
|
+
this.wasm = wasm;
|
|
357
|
+
this.bridge = bridge;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Build a text layer DOM structure inside the given container.
|
|
361
|
+
* Spans are absolutely positioned to match the rendered PDF pixels.
|
|
362
|
+
*
|
|
363
|
+
* @param textPagePtr - PDFium text page handle
|
|
364
|
+
* @param container - DOM element to append text layer into
|
|
365
|
+
* @param pageWidth - PDF page width in points
|
|
366
|
+
* @param pageHeight - PDF page height in points
|
|
367
|
+
* @param scale - Rendering scale factor (default 1.0)
|
|
368
|
+
*/
|
|
369
|
+
build(textPagePtr, container, pageWidth, pageHeight, scale = 1) {
|
|
370
|
+
const layer = document.createElement("div");
|
|
371
|
+
layer.className = "pdfnova-text-layer";
|
|
372
|
+
layer.style.cssText = `
|
|
373
|
+
position: absolute;
|
|
374
|
+
top: 0;
|
|
375
|
+
left: 0;
|
|
376
|
+
width: ${pageWidth * scale}px;
|
|
377
|
+
height: ${pageHeight * scale}px;
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
opacity: 0.25;
|
|
380
|
+
line-height: 1;
|
|
381
|
+
pointer-events: all;
|
|
382
|
+
`;
|
|
383
|
+
const extractor = new TextExtractor(this.wasm, this.bridge);
|
|
384
|
+
const spans = extractor.extractSpans(textPagePtr);
|
|
385
|
+
for (const span of spans) {
|
|
386
|
+
const el = this._createSpanElement(span, pageHeight, scale);
|
|
387
|
+
layer.appendChild(el);
|
|
388
|
+
}
|
|
389
|
+
container.appendChild(layer);
|
|
390
|
+
return layer;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Build a text layer using character-level positioning for maximum accuracy.
|
|
394
|
+
* Slower but pixel-perfect — use when precise text selection is critical.
|
|
395
|
+
*/
|
|
396
|
+
buildCharLevel(textPagePtr, container, pageWidth, pageHeight, scale = 1) {
|
|
397
|
+
const layer = document.createElement("div");
|
|
398
|
+
layer.className = "pdfnova-text-layer pdfnova-text-layer--char-level";
|
|
399
|
+
layer.style.cssText = `
|
|
400
|
+
position: absolute;
|
|
401
|
+
top: 0;
|
|
402
|
+
left: 0;
|
|
403
|
+
width: ${pageWidth * scale}px;
|
|
404
|
+
height: ${pageHeight * scale}px;
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
opacity: 0.25;
|
|
407
|
+
line-height: 1;
|
|
408
|
+
pointer-events: all;
|
|
409
|
+
`;
|
|
410
|
+
const extractor = new TextExtractor(this.wasm, this.bridge);
|
|
411
|
+
const charBoxes = extractor.extractCharBoxes(textPagePtr);
|
|
412
|
+
const runs = this._groupIntoRuns(charBoxes);
|
|
413
|
+
for (const run of runs) {
|
|
414
|
+
const runEl = document.createElement("span");
|
|
415
|
+
runEl.style.cssText = `
|
|
416
|
+
position: absolute;
|
|
417
|
+
left: ${run[0].left * scale}px;
|
|
418
|
+
top: ${(pageHeight - run[0].top) * scale}px;
|
|
419
|
+
font-size: ${run[0].fontSize * scale}px;
|
|
420
|
+
font-family: sans-serif;
|
|
421
|
+
white-space: pre;
|
|
422
|
+
transform-origin: 0% 0%;
|
|
423
|
+
`;
|
|
424
|
+
let text = "";
|
|
425
|
+
for (const box of run) {
|
|
426
|
+
text += box.char;
|
|
427
|
+
}
|
|
428
|
+
runEl.textContent = text;
|
|
429
|
+
const firstBox = run[0];
|
|
430
|
+
const lastBox = run[run.length - 1];
|
|
431
|
+
const expectedWidth = (lastBox.right - firstBox.left) * scale;
|
|
432
|
+
if (expectedWidth > 0 && runEl.textContent.length > 0) {
|
|
433
|
+
runEl.dataset.expectedWidth = String(expectedWidth);
|
|
434
|
+
}
|
|
435
|
+
layer.appendChild(runEl);
|
|
436
|
+
}
|
|
437
|
+
container.appendChild(layer);
|
|
438
|
+
return layer;
|
|
439
|
+
}
|
|
440
|
+
_createSpanElement(span, pageHeight, scale) {
|
|
441
|
+
const el = document.createElement("span");
|
|
442
|
+
const domX = span.x * scale;
|
|
443
|
+
const domY = (pageHeight - span.y - span.height) * scale;
|
|
444
|
+
const fontSize = span.fontSize * scale;
|
|
445
|
+
el.textContent = span.text;
|
|
446
|
+
el.style.cssText = `
|
|
447
|
+
position: absolute;
|
|
448
|
+
left: ${domX}px;
|
|
449
|
+
top: ${domY}px;
|
|
450
|
+
font-size: ${fontSize}px;
|
|
451
|
+
font-family: sans-serif;
|
|
452
|
+
white-space: pre;
|
|
453
|
+
transform-origin: 0% 0%;
|
|
454
|
+
`;
|
|
455
|
+
el.dataset.charIndex = String(span.charIndex);
|
|
456
|
+
el.dataset.charCount = String(span.charCount);
|
|
457
|
+
return el;
|
|
458
|
+
}
|
|
459
|
+
_groupIntoRuns(boxes) {
|
|
460
|
+
if (boxes.length === 0) return [];
|
|
461
|
+
const runs = [];
|
|
462
|
+
let currentRun = [boxes[0]];
|
|
463
|
+
for (let i = 1; i < boxes.length; i++) {
|
|
464
|
+
const prev = boxes[i - 1];
|
|
465
|
+
const curr = boxes[i];
|
|
466
|
+
const sameLine = Math.abs(prev.bottom - curr.bottom) < prev.fontSize * 0.5;
|
|
467
|
+
const gap = curr.left - prev.right;
|
|
468
|
+
const tooFar = gap > prev.fontSize * 2;
|
|
469
|
+
if (sameLine && !tooFar) {
|
|
470
|
+
currentRun.push(curr);
|
|
471
|
+
} else {
|
|
472
|
+
if (currentRun.length > 0) runs.push(currentRun);
|
|
473
|
+
currentRun = [curr];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (currentRun.length > 0) runs.push(currentRun);
|
|
477
|
+
return runs;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/text/SearchEngine.ts
|
|
482
|
+
var SearchEngine = class {
|
|
483
|
+
wasm;
|
|
484
|
+
bridge;
|
|
485
|
+
constructor(wasm, bridge) {
|
|
486
|
+
this.wasm = wasm;
|
|
487
|
+
this.bridge = bridge;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Search across all pages of a document.
|
|
491
|
+
*/
|
|
492
|
+
searchDocument(docPtr, pageCount, query, options) {
|
|
493
|
+
if (!query) return [];
|
|
494
|
+
const allResults = [];
|
|
495
|
+
let globalMatchIndex = 0;
|
|
496
|
+
for (let i = 0; i < pageCount; i++) {
|
|
497
|
+
const pagePtr = this.wasm._FPDF_LoadPage(docPtr, i);
|
|
498
|
+
if (pagePtr === 0) continue;
|
|
499
|
+
const textPagePtr = this.wasm._FPDFText_LoadPage(pagePtr);
|
|
500
|
+
if (textPagePtr === 0) {
|
|
501
|
+
this.wasm._FPDF_ClosePage(pagePtr);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const pageResults = this.searchPage(textPagePtr, i, query, options);
|
|
506
|
+
for (const result of pageResults) {
|
|
507
|
+
result.matchIndex = globalMatchIndex++;
|
|
508
|
+
allResults.push(result);
|
|
509
|
+
}
|
|
510
|
+
} finally {
|
|
511
|
+
this.wasm._FPDFText_ClosePage(textPagePtr);
|
|
512
|
+
this.wasm._FPDF_ClosePage(pagePtr);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return allResults;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Search within a single page's text content.
|
|
519
|
+
*/
|
|
520
|
+
searchPage(textPagePtr, pageIndex, query, options) {
|
|
521
|
+
if (!query) return [];
|
|
522
|
+
const results = [];
|
|
523
|
+
let flags = 0;
|
|
524
|
+
if (options?.caseSensitive) flags |= 1;
|
|
525
|
+
if (options?.wholeWord) flags |= 2;
|
|
526
|
+
const queryPtr = this.bridge.allocateUTF16String(query);
|
|
527
|
+
try {
|
|
528
|
+
const findHandle = this.wasm._FPDFText_FindStart(textPagePtr, queryPtr, flags, 0);
|
|
529
|
+
if (findHandle === 0) return results;
|
|
530
|
+
try {
|
|
531
|
+
while (this.wasm._FPDFText_FindNext(findHandle)) {
|
|
532
|
+
const charIndex = this.wasm._FPDFText_GetSchResultIndex(findHandle);
|
|
533
|
+
const count = this.wasm._FPDFText_GetSchCount(findHandle);
|
|
534
|
+
if (charIndex >= 0 && count > 0) {
|
|
535
|
+
results.push({
|
|
536
|
+
pageIndex,
|
|
537
|
+
matchIndex: results.length,
|
|
538
|
+
charIndex,
|
|
539
|
+
charCount: count,
|
|
540
|
+
rects: this._getMatchRects(textPagePtr, charIndex, count),
|
|
541
|
+
text: this._getMatchText(textPagePtr, charIndex, count)
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} finally {
|
|
546
|
+
this.wasm._FPDFText_FindClose(findHandle);
|
|
547
|
+
}
|
|
548
|
+
} finally {
|
|
549
|
+
this.bridge.free(queryPtr);
|
|
550
|
+
}
|
|
551
|
+
return results;
|
|
552
|
+
}
|
|
553
|
+
_getMatchRects(textPagePtr, charIndex, charCount) {
|
|
554
|
+
const numRects = this.wasm._FPDFText_CountRects(textPagePtr, charIndex, charCount);
|
|
555
|
+
if (numRects <= 0) return [];
|
|
556
|
+
const rects = [];
|
|
557
|
+
const leftPtr = this.bridge.allocateF64();
|
|
558
|
+
const topPtr = this.bridge.allocateF64();
|
|
559
|
+
const rightPtr = this.bridge.allocateF64();
|
|
560
|
+
const bottomPtr = this.bridge.allocateF64();
|
|
561
|
+
try {
|
|
562
|
+
for (let i = 0; i < numRects; i++) {
|
|
563
|
+
this.wasm._FPDFText_GetRect(textPagePtr, i, leftPtr, topPtr, rightPtr, bottomPtr);
|
|
564
|
+
rects.push({
|
|
565
|
+
left: this.bridge.readF64(leftPtr),
|
|
566
|
+
top: this.bridge.readF64(topPtr),
|
|
567
|
+
right: this.bridge.readF64(rightPtr),
|
|
568
|
+
bottom: this.bridge.readF64(bottomPtr)
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} finally {
|
|
572
|
+
this.bridge.freeAll(leftPtr, topPtr, rightPtr, bottomPtr);
|
|
573
|
+
}
|
|
574
|
+
return rects;
|
|
575
|
+
}
|
|
576
|
+
_getMatchText(textPagePtr, charIndex, charCount) {
|
|
577
|
+
const bufSize = (charCount + 1) * 2;
|
|
578
|
+
const buf = this.wasm._malloc(bufSize);
|
|
579
|
+
this.wasm._FPDFText_GetText(textPagePtr, charIndex, charCount, buf);
|
|
580
|
+
const text = this.bridge.readUTF16String(buf);
|
|
581
|
+
this.wasm._free(buf);
|
|
582
|
+
return text;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/navigation/LinkExtractor.ts
|
|
587
|
+
var LinkExtractor = class {
|
|
588
|
+
wasm;
|
|
589
|
+
bridge;
|
|
590
|
+
constructor(wasm, bridge) {
|
|
591
|
+
this.wasm = wasm;
|
|
592
|
+
this.bridge = bridge;
|
|
593
|
+
}
|
|
594
|
+
extractLinks(textPagePtr, pageIndex) {
|
|
595
|
+
const linkPageHandle = this.wasm._FPDFLink_LoadWebLinks(textPagePtr);
|
|
596
|
+
if (linkPageHandle === 0) return [];
|
|
597
|
+
const links = [];
|
|
598
|
+
try {
|
|
599
|
+
const count = this.wasm._FPDFLink_CountWebLinks(linkPageHandle);
|
|
600
|
+
for (let i = 0; i < count; i++) {
|
|
601
|
+
const url = this.bridge.getLinkURL(linkPageHandle, i);
|
|
602
|
+
if (!url) continue;
|
|
603
|
+
const rects = this._getLinkRects(linkPageHandle, i);
|
|
604
|
+
links.push({ url, rects, pageIndex });
|
|
605
|
+
}
|
|
606
|
+
} finally {
|
|
607
|
+
this.wasm._FPDFLink_CloseWebLinks(linkPageHandle);
|
|
608
|
+
}
|
|
609
|
+
return links;
|
|
610
|
+
}
|
|
611
|
+
_getLinkRects(linkPageHandle, linkIndex) {
|
|
612
|
+
const rectCount = this.wasm._FPDFLink_CountRects(linkPageHandle, linkIndex);
|
|
613
|
+
if (rectCount <= 0) return [];
|
|
614
|
+
const rects = [];
|
|
615
|
+
const leftPtr = this.bridge.allocateF64();
|
|
616
|
+
const topPtr = this.bridge.allocateF64();
|
|
617
|
+
const rightPtr = this.bridge.allocateF64();
|
|
618
|
+
const bottomPtr = this.bridge.allocateF64();
|
|
619
|
+
try {
|
|
620
|
+
for (let i = 0; i < rectCount; i++) {
|
|
621
|
+
this.wasm._FPDFLink_GetRect(linkPageHandle, linkIndex, i, leftPtr, topPtr, rightPtr, bottomPtr);
|
|
622
|
+
rects.push({
|
|
623
|
+
left: this.bridge.readF64(leftPtr),
|
|
624
|
+
top: this.bridge.readF64(topPtr),
|
|
625
|
+
right: this.bridge.readF64(rightPtr),
|
|
626
|
+
bottom: this.bridge.readF64(bottomPtr)
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
} finally {
|
|
630
|
+
this.bridge.freeAll(leftPtr, topPtr, rightPtr, bottomPtr);
|
|
631
|
+
}
|
|
632
|
+
return rects;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/capabilities.ts
|
|
637
|
+
var currentTier = "lite";
|
|
638
|
+
function setTier(tier) {
|
|
639
|
+
currentTier = tier;
|
|
640
|
+
}
|
|
641
|
+
function getTier() {
|
|
642
|
+
return currentTier;
|
|
643
|
+
}
|
|
644
|
+
function isFullTier() {
|
|
645
|
+
return currentTier === "full";
|
|
646
|
+
}
|
|
647
|
+
function requireFull(feature) {
|
|
648
|
+
if (currentTier !== "full") {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`${feature} requires the full build of pdfnova. Import from 'pdfnova' instead of 'pdfnova/lite'.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/core/MemoryManager.ts
|
|
656
|
+
var MemoryManager = class {
|
|
657
|
+
wasm;
|
|
658
|
+
allocations = [];
|
|
659
|
+
disposed = false;
|
|
660
|
+
constructor(wasm) {
|
|
661
|
+
this.wasm = wasm;
|
|
662
|
+
}
|
|
663
|
+
alloc(size, label) {
|
|
664
|
+
if (this.disposed) throw new Error("MemoryManager has been disposed");
|
|
665
|
+
const ptr = this.wasm._malloc(size);
|
|
666
|
+
if (ptr === 0) throw new Error(`WASM allocation failed: ${size} bytes`);
|
|
667
|
+
this.allocations.push({ ptr, size, label });
|
|
668
|
+
return ptr;
|
|
669
|
+
}
|
|
670
|
+
allocF64(label) {
|
|
671
|
+
return this.alloc(8, label);
|
|
672
|
+
}
|
|
673
|
+
allocI32(label) {
|
|
674
|
+
return this.alloc(4, label);
|
|
675
|
+
}
|
|
676
|
+
allocBytes(data, label) {
|
|
677
|
+
const ptr = this.alloc(data.byteLength, label);
|
|
678
|
+
this.wasm.HEAPU8.set(data, ptr);
|
|
679
|
+
return ptr;
|
|
680
|
+
}
|
|
681
|
+
allocString(str, label) {
|
|
682
|
+
const encoded = new TextEncoder().encode(str);
|
|
683
|
+
const ptr = this.alloc(encoded.length + 1, label);
|
|
684
|
+
this.wasm.HEAPU8.set(encoded, ptr);
|
|
685
|
+
this.wasm.HEAPU8[ptr + encoded.length] = 0;
|
|
686
|
+
return ptr;
|
|
687
|
+
}
|
|
688
|
+
allocUTF16(str, label) {
|
|
689
|
+
const byteLen = (str.length + 1) * 2;
|
|
690
|
+
const ptr = this.alloc(byteLen, label);
|
|
691
|
+
this.wasm.stringToUTF16(str, ptr, byteLen);
|
|
692
|
+
return ptr;
|
|
693
|
+
}
|
|
694
|
+
readF64(ptr) {
|
|
695
|
+
return this.wasm.HEAPF64[ptr >> 3];
|
|
696
|
+
}
|
|
697
|
+
readI32(ptr) {
|
|
698
|
+
return this.wasm.HEAP32[ptr >> 2];
|
|
699
|
+
}
|
|
700
|
+
free(ptr) {
|
|
701
|
+
const idx = this.allocations.findIndex((a) => a.ptr === ptr);
|
|
702
|
+
if (idx >= 0) {
|
|
703
|
+
this.allocations.splice(idx, 1);
|
|
704
|
+
}
|
|
705
|
+
this.wasm._free(ptr);
|
|
706
|
+
}
|
|
707
|
+
dispose() {
|
|
708
|
+
if (this.disposed) return;
|
|
709
|
+
this.disposed = true;
|
|
710
|
+
for (const { ptr } of this.allocations) {
|
|
711
|
+
try {
|
|
712
|
+
this.wasm._free(ptr);
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
this.allocations.length = 0;
|
|
717
|
+
}
|
|
718
|
+
get allocationCount() {
|
|
719
|
+
return this.allocations.length;
|
|
720
|
+
}
|
|
721
|
+
get totalAllocated() {
|
|
722
|
+
return this.allocations.reduce((sum, a) => sum + a.size, 0);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// src/document/PDFPage.ts
|
|
727
|
+
var PDFPage = class _PDFPage {
|
|
728
|
+
wasm;
|
|
729
|
+
bridge;
|
|
730
|
+
docPtr;
|
|
731
|
+
_pageIndex;
|
|
732
|
+
pagePtr = 0;
|
|
733
|
+
textPagePtr = 0;
|
|
734
|
+
_formHandle;
|
|
735
|
+
_closed = false;
|
|
736
|
+
mem;
|
|
737
|
+
constructor(wasm, bridge, docPtr, pageIndex, formHandle) {
|
|
738
|
+
this.wasm = wasm;
|
|
739
|
+
this.bridge = bridge;
|
|
740
|
+
this.docPtr = docPtr;
|
|
741
|
+
this._pageIndex = pageIndex;
|
|
742
|
+
this._formHandle = formHandle;
|
|
743
|
+
this.mem = new MemoryManager(wasm);
|
|
744
|
+
this._load();
|
|
745
|
+
}
|
|
746
|
+
_load() {
|
|
747
|
+
this.pagePtr = this.wasm._FPDF_LoadPage(this.docPtr, this._pageIndex);
|
|
748
|
+
if (this.pagePtr === 0) {
|
|
749
|
+
throw new Error(`Failed to load page ${this._pageIndex}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// ─── Properties ──────────────────────────────────────────────
|
|
753
|
+
get pageIndex() {
|
|
754
|
+
return this._pageIndex;
|
|
755
|
+
}
|
|
756
|
+
get formHandle() {
|
|
757
|
+
return this._formHandle;
|
|
758
|
+
}
|
|
759
|
+
get width() {
|
|
760
|
+
this._assertOpen();
|
|
761
|
+
return this.wasm._FPDF_GetPageWidthF(this.pagePtr);
|
|
762
|
+
}
|
|
763
|
+
get height() {
|
|
764
|
+
this._assertOpen();
|
|
765
|
+
return this.wasm._FPDF_GetPageHeightF(this.pagePtr);
|
|
766
|
+
}
|
|
767
|
+
// ─── Rendering ───────────────────────────────────────────────
|
|
768
|
+
async render(canvas, options) {
|
|
769
|
+
this._assertOpen();
|
|
770
|
+
const scale = options?.scale ?? 1;
|
|
771
|
+
const rotation = options?.rotation ?? 0;
|
|
772
|
+
const rotationIndex = [0, 90, 180, 270].indexOf(rotation);
|
|
773
|
+
if (rotationIndex < 0) throw new RangeError("Rotation must be 0, 90, 180, or 270");
|
|
774
|
+
const isRotated90or270 = rotation === 90 || rotation === 270;
|
|
775
|
+
const pdfW = this.width;
|
|
776
|
+
const pdfH = this.height;
|
|
777
|
+
const renderW = Math.ceil((isRotated90or270 ? pdfH : pdfW) * scale);
|
|
778
|
+
const renderH = Math.ceil((isRotated90or270 ? pdfW : pdfH) * scale);
|
|
779
|
+
canvas.width = renderW;
|
|
780
|
+
canvas.height = renderH;
|
|
781
|
+
const baseFlags = options?.flags ?? RENDER_FLAG.ANNOT | RENDER_FLAG.LCD_TEXT;
|
|
782
|
+
const flags = baseFlags | RENDER_FLAG.REVERSE_BYTE_ORDER;
|
|
783
|
+
const bitmapHandle = this.wasm._FPDFBitmap_Create(renderW, renderH, 0);
|
|
784
|
+
if (bitmapHandle === 0) throw new Error("Failed to create bitmap");
|
|
785
|
+
try {
|
|
786
|
+
const bgColor = _PDFPage._parseColor(options?.background ?? "#ffffff");
|
|
787
|
+
this.wasm._FPDFBitmap_FillRect(bitmapHandle, 0, 0, renderW, renderH, bgColor);
|
|
788
|
+
this.wasm._FPDF_RenderPageBitmap(
|
|
789
|
+
bitmapHandle,
|
|
790
|
+
this.pagePtr,
|
|
791
|
+
0,
|
|
792
|
+
0,
|
|
793
|
+
renderW,
|
|
794
|
+
renderH,
|
|
795
|
+
rotationIndex,
|
|
796
|
+
flags
|
|
797
|
+
);
|
|
798
|
+
const bufferPtr = this.wasm._FPDFBitmap_GetBuffer(bitmapHandle);
|
|
799
|
+
const stride = this.wasm._FPDFBitmap_GetStride(bitmapHandle);
|
|
800
|
+
const ctx = canvas.getContext("2d");
|
|
801
|
+
if (!ctx) throw new Error("Failed to get canvas 2D context");
|
|
802
|
+
const imageData = ctx.createImageData(renderW, renderH);
|
|
803
|
+
const dst = imageData.data;
|
|
804
|
+
const rowBytes = renderW * 4;
|
|
805
|
+
for (let y = 0; y < renderH; y++) {
|
|
806
|
+
const srcOffset = bufferPtr + y * stride;
|
|
807
|
+
const dstOffset = y * rowBytes;
|
|
808
|
+
dst.set(this.wasm.HEAPU8.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
809
|
+
for (let x = 3; x < rowBytes; x += 4) {
|
|
810
|
+
dst[dstOffset + x] = 255;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
ctx.putImageData(imageData, 0, 0);
|
|
814
|
+
} finally {
|
|
815
|
+
this.wasm._FPDFBitmap_Destroy(bitmapHandle);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async renderToImageData(options) {
|
|
819
|
+
this._assertOpen();
|
|
820
|
+
const scale = options?.scale ?? 1;
|
|
821
|
+
const rotation = options?.rotation ?? 0;
|
|
822
|
+
const rotationIndex = [0, 90, 180, 270].indexOf(rotation);
|
|
823
|
+
if (rotationIndex < 0) throw new RangeError("Rotation must be 0, 90, 180, or 270");
|
|
824
|
+
const isRotated = rotation === 90 || rotation === 270;
|
|
825
|
+
const renderW = Math.ceil((isRotated ? this.height : this.width) * scale);
|
|
826
|
+
const renderH = Math.ceil((isRotated ? this.width : this.height) * scale);
|
|
827
|
+
const baseFlags = options?.flags ?? RENDER_FLAG.ANNOT | RENDER_FLAG.LCD_TEXT;
|
|
828
|
+
const flags = baseFlags | RENDER_FLAG.REVERSE_BYTE_ORDER;
|
|
829
|
+
const bitmapHandle = this.wasm._FPDFBitmap_Create(renderW, renderH, 0);
|
|
830
|
+
if (bitmapHandle === 0) throw new Error("Failed to create bitmap");
|
|
831
|
+
try {
|
|
832
|
+
const bgColor = _PDFPage._parseColor(options?.background ?? "#ffffff");
|
|
833
|
+
this.wasm._FPDFBitmap_FillRect(bitmapHandle, 0, 0, renderW, renderH, bgColor);
|
|
834
|
+
this.wasm._FPDF_RenderPageBitmap(
|
|
835
|
+
bitmapHandle,
|
|
836
|
+
this.pagePtr,
|
|
837
|
+
0,
|
|
838
|
+
0,
|
|
839
|
+
renderW,
|
|
840
|
+
renderH,
|
|
841
|
+
rotationIndex,
|
|
842
|
+
flags
|
|
843
|
+
);
|
|
844
|
+
const bufferPtr = this.wasm._FPDFBitmap_GetBuffer(bitmapHandle);
|
|
845
|
+
const stride = this.wasm._FPDFBitmap_GetStride(bitmapHandle);
|
|
846
|
+
const data = new Uint8ClampedArray(renderW * renderH * 4);
|
|
847
|
+
const rowBytes = renderW * 4;
|
|
848
|
+
for (let y = 0; y < renderH; y++) {
|
|
849
|
+
const srcOffset = bufferPtr + y * stride;
|
|
850
|
+
const dstOffset = y * rowBytes;
|
|
851
|
+
data.set(this.wasm.HEAPU8.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
852
|
+
for (let x = 3; x < rowBytes; x += 4) {
|
|
853
|
+
data[dstOffset + x] = 255;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return new ImageData(data, renderW, renderH);
|
|
857
|
+
} finally {
|
|
858
|
+
this.wasm._FPDFBitmap_Destroy(bitmapHandle);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// ─── Text ────────────────────────────────────────────────────
|
|
862
|
+
getText() {
|
|
863
|
+
this._assertOpen();
|
|
864
|
+
this._ensureTextPage();
|
|
865
|
+
const extractor = new TextExtractor(this.wasm, this.bridge);
|
|
866
|
+
return extractor.extractText(this.textPagePtr);
|
|
867
|
+
}
|
|
868
|
+
getTextSpans() {
|
|
869
|
+
this._assertOpen();
|
|
870
|
+
this._ensureTextPage();
|
|
871
|
+
const extractor = new TextExtractor(this.wasm, this.bridge);
|
|
872
|
+
return extractor.extractSpans(this.textPagePtr);
|
|
873
|
+
}
|
|
874
|
+
getCharBoxes() {
|
|
875
|
+
this._assertOpen();
|
|
876
|
+
this._ensureTextPage();
|
|
877
|
+
const extractor = new TextExtractor(this.wasm, this.bridge);
|
|
878
|
+
return extractor.extractCharBoxes(this.textPagePtr);
|
|
879
|
+
}
|
|
880
|
+
createTextLayer(container) {
|
|
881
|
+
this._assertOpen();
|
|
882
|
+
this._ensureTextPage();
|
|
883
|
+
const builder = new TextLayerBuilder(this.wasm, this.bridge);
|
|
884
|
+
return builder.build(this.textPagePtr, container, this.width, this.height);
|
|
885
|
+
}
|
|
886
|
+
// ─── Search ──────────────────────────────────────────────────
|
|
887
|
+
search(query, options) {
|
|
888
|
+
this._assertOpen();
|
|
889
|
+
this._ensureTextPage();
|
|
890
|
+
const engine = new SearchEngine(this.wasm, this.bridge);
|
|
891
|
+
return engine.searchPage(this.textPagePtr, this._pageIndex, query, options);
|
|
892
|
+
}
|
|
893
|
+
// ─── Links ───────────────────────────────────────────────────
|
|
894
|
+
getLinks() {
|
|
895
|
+
this._assertOpen();
|
|
896
|
+
this._ensureTextPage();
|
|
897
|
+
const extractor = new LinkExtractor(this.wasm, this.bridge);
|
|
898
|
+
return extractor.extractLinks(this.textPagePtr, this._pageIndex);
|
|
899
|
+
}
|
|
900
|
+
// ─── Annotations (full tier) ─────────────────────────────────
|
|
901
|
+
async getAnnotations() {
|
|
902
|
+
requireFull("Annotation reading");
|
|
903
|
+
this._assertOpen();
|
|
904
|
+
const { AnnotationReader } = await import('./AnnotationReader-ADHPZ3V5.mjs');
|
|
905
|
+
const reader = new AnnotationReader(this.wasm, this.bridge);
|
|
906
|
+
return reader.readAnnotations(this.pagePtr);
|
|
907
|
+
}
|
|
908
|
+
async addAnnotation(opts) {
|
|
909
|
+
requireFull("Annotation creation");
|
|
910
|
+
this._assertOpen();
|
|
911
|
+
const { AnnotationWriter } = await import('./AnnotationWriter-XWY5N6AZ.mjs');
|
|
912
|
+
const writer = new AnnotationWriter(this.wasm, this.bridge);
|
|
913
|
+
writer.addAnnotation(this.pagePtr, opts);
|
|
914
|
+
}
|
|
915
|
+
async removeAnnotation(index) {
|
|
916
|
+
requireFull("Annotation removal");
|
|
917
|
+
this._assertOpen();
|
|
918
|
+
const { AnnotationWriter } = await import('./AnnotationWriter-XWY5N6AZ.mjs');
|
|
919
|
+
const writer = new AnnotationWriter(this.wasm, this.bridge);
|
|
920
|
+
writer.removeAnnotation(this.pagePtr, index);
|
|
921
|
+
}
|
|
922
|
+
// ─── Cleanup ─────────────────────────────────────────────────
|
|
923
|
+
close() {
|
|
924
|
+
if (this._closed) return;
|
|
925
|
+
this._closed = true;
|
|
926
|
+
if (this.textPagePtr) {
|
|
927
|
+
this.wasm._FPDFText_ClosePage(this.textPagePtr);
|
|
928
|
+
this.textPagePtr = 0;
|
|
929
|
+
}
|
|
930
|
+
if (this.pagePtr) {
|
|
931
|
+
this.wasm._FPDF_ClosePage(this.pagePtr);
|
|
932
|
+
this.pagePtr = 0;
|
|
933
|
+
}
|
|
934
|
+
this.mem.dispose();
|
|
935
|
+
}
|
|
936
|
+
[Symbol.dispose]() {
|
|
937
|
+
this.close();
|
|
938
|
+
}
|
|
939
|
+
// ─── Internal ────────────────────────────────────────────────
|
|
940
|
+
_ensureTextPage() {
|
|
941
|
+
if (this.textPagePtr) return;
|
|
942
|
+
this.textPagePtr = this.wasm._FPDFText_LoadPage(this.pagePtr);
|
|
943
|
+
if (this.textPagePtr === 0) {
|
|
944
|
+
throw new Error(`Failed to load text page for page ${this._pageIndex}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
_assertOpen() {
|
|
948
|
+
if (this._closed) throw new Error("Page has been closed");
|
|
949
|
+
}
|
|
950
|
+
static _parseColor(hex) {
|
|
951
|
+
const clean = hex.replace("#", "");
|
|
952
|
+
const r = parseInt(clean.slice(0, 2), 16);
|
|
953
|
+
const g = parseInt(clean.slice(2, 4), 16);
|
|
954
|
+
const b = parseInt(clean.slice(4, 6), 16);
|
|
955
|
+
const a = clean.length > 6 ? parseInt(clean.slice(6, 8), 16) : 255;
|
|
956
|
+
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/navigation/OutlineExtractor.ts
|
|
961
|
+
var OutlineExtractor = class {
|
|
962
|
+
wasm;
|
|
963
|
+
bridge;
|
|
964
|
+
constructor(wasm, bridge) {
|
|
965
|
+
this.wasm = wasm;
|
|
966
|
+
this.bridge = bridge;
|
|
967
|
+
}
|
|
968
|
+
extract(docPtr) {
|
|
969
|
+
return this._walkBookmarks(docPtr, 0);
|
|
970
|
+
}
|
|
971
|
+
_walkBookmarks(docPtr, parentBookmark) {
|
|
972
|
+
const items = [];
|
|
973
|
+
let bookmark = this.wasm._FPDFBookmark_GetFirstChild(docPtr, parentBookmark);
|
|
974
|
+
while (bookmark !== 0) {
|
|
975
|
+
const title = this.bridge.getBookmarkTitle(bookmark);
|
|
976
|
+
let pageIndex = -1;
|
|
977
|
+
const dest = this.wasm._FPDFBookmark_GetDest(docPtr, bookmark);
|
|
978
|
+
if (dest !== 0) {
|
|
979
|
+
pageIndex = this.wasm._FPDFDest_GetDestPageIndex(docPtr, dest);
|
|
980
|
+
} else {
|
|
981
|
+
this.wasm._FPDFBookmark_GetAction(bookmark);
|
|
982
|
+
}
|
|
983
|
+
const children = this._walkBookmarks(docPtr, bookmark);
|
|
984
|
+
items.push({ title, pageIndex, children });
|
|
985
|
+
bookmark = this.wasm._FPDFBookmark_GetNextSibling(docPtr, bookmark);
|
|
986
|
+
}
|
|
987
|
+
return items;
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/core/WasmBridge.ts
|
|
992
|
+
var WasmBridge = class {
|
|
993
|
+
wasm;
|
|
994
|
+
constructor(wasm) {
|
|
995
|
+
this.wasm = wasm ?? WasmLoader.getInstance();
|
|
996
|
+
}
|
|
997
|
+
get module() {
|
|
998
|
+
return this.wasm;
|
|
999
|
+
}
|
|
1000
|
+
allocateString(str) {
|
|
1001
|
+
const len = this.wasm.lengthBytesUTF8(str) + 1;
|
|
1002
|
+
const ptr = this.wasm._malloc(len);
|
|
1003
|
+
this.wasm.stringToUTF8(str, ptr, len);
|
|
1004
|
+
return ptr;
|
|
1005
|
+
}
|
|
1006
|
+
allocateUTF16String(str) {
|
|
1007
|
+
const byteLen = (str.length + 1) * 2;
|
|
1008
|
+
const ptr = this.wasm._malloc(byteLen);
|
|
1009
|
+
this.wasm.stringToUTF16(str, ptr, byteLen);
|
|
1010
|
+
return ptr;
|
|
1011
|
+
}
|
|
1012
|
+
readString(ptr) {
|
|
1013
|
+
return this.wasm.UTF8ToString(ptr);
|
|
1014
|
+
}
|
|
1015
|
+
readUTF16String(ptr) {
|
|
1016
|
+
return this.wasm.UTF16ToString(ptr);
|
|
1017
|
+
}
|
|
1018
|
+
copyToHeap(data) {
|
|
1019
|
+
const ptr = this.wasm._malloc(data.byteLength);
|
|
1020
|
+
this.wasm.HEAPU8.set(data, ptr);
|
|
1021
|
+
return ptr;
|
|
1022
|
+
}
|
|
1023
|
+
copyFromHeap(ptr, size) {
|
|
1024
|
+
return new Uint8Array(this.wasm.HEAPU8.buffer, ptr, size).slice();
|
|
1025
|
+
}
|
|
1026
|
+
allocateF64() {
|
|
1027
|
+
return this.wasm._malloc(8);
|
|
1028
|
+
}
|
|
1029
|
+
readF64(ptr) {
|
|
1030
|
+
return this.wasm.HEAPF64[ptr >> 3];
|
|
1031
|
+
}
|
|
1032
|
+
allocateI32() {
|
|
1033
|
+
return this.wasm._malloc(4);
|
|
1034
|
+
}
|
|
1035
|
+
readI32(ptr) {
|
|
1036
|
+
return this.wasm.HEAP32[ptr >> 2];
|
|
1037
|
+
}
|
|
1038
|
+
free(ptr) {
|
|
1039
|
+
this.wasm._free(ptr);
|
|
1040
|
+
}
|
|
1041
|
+
freeAll(...ptrs) {
|
|
1042
|
+
for (const ptr of ptrs) {
|
|
1043
|
+
if (ptr) this.wasm._free(ptr);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
getMetaText(docPtr, tag) {
|
|
1047
|
+
const tagPtr = this.allocateString(tag);
|
|
1048
|
+
const needed = this.wasm._FPDF_GetMetaText(docPtr, tagPtr, 0, 0);
|
|
1049
|
+
if (needed <= 0) {
|
|
1050
|
+
this.free(tagPtr);
|
|
1051
|
+
return "";
|
|
1052
|
+
}
|
|
1053
|
+
const buf = this.wasm._malloc(needed);
|
|
1054
|
+
this.wasm._FPDF_GetMetaText(docPtr, tagPtr, buf, needed);
|
|
1055
|
+
const value = this.readUTF16String(buf);
|
|
1056
|
+
this.freeAll(tagPtr, buf);
|
|
1057
|
+
return value;
|
|
1058
|
+
}
|
|
1059
|
+
getBookmarkTitle(bookmark) {
|
|
1060
|
+
const needed = this.wasm._FPDFBookmark_GetTitle(bookmark, 0, 0);
|
|
1061
|
+
if (needed <= 0) return "";
|
|
1062
|
+
const buf = this.wasm._malloc(needed);
|
|
1063
|
+
this.wasm._FPDFBookmark_GetTitle(bookmark, buf, needed);
|
|
1064
|
+
const title = this.readUTF16String(buf);
|
|
1065
|
+
this.free(buf);
|
|
1066
|
+
return title;
|
|
1067
|
+
}
|
|
1068
|
+
getLinkURL(linkPage, linkIndex) {
|
|
1069
|
+
const needed = this.wasm._FPDFLink_GetURL(linkPage, linkIndex, 0, 0);
|
|
1070
|
+
if (needed <= 0) return "";
|
|
1071
|
+
const buf = this.wasm._malloc(needed * 2);
|
|
1072
|
+
this.wasm._FPDFLink_GetURL(linkPage, linkIndex, buf, needed);
|
|
1073
|
+
const url = this.readUTF16String(buf);
|
|
1074
|
+
this.free(buf);
|
|
1075
|
+
return url;
|
|
1076
|
+
}
|
|
1077
|
+
getAnnotStringValue(annot, key) {
|
|
1078
|
+
if (!this.wasm._FPDFAnnot_GetStringValue) return "";
|
|
1079
|
+
const keyPtr = this.allocateString(key);
|
|
1080
|
+
const needed = this.wasm._FPDFAnnot_GetStringValue(annot, keyPtr, 0, 0);
|
|
1081
|
+
if (needed <= 0) {
|
|
1082
|
+
this.free(keyPtr);
|
|
1083
|
+
return "";
|
|
1084
|
+
}
|
|
1085
|
+
const buf = this.wasm._malloc(needed);
|
|
1086
|
+
this.wasm._FPDFAnnot_GetStringValue(annot, keyPtr, buf, needed);
|
|
1087
|
+
const value = this.readUTF16String(buf);
|
|
1088
|
+
this.freeAll(keyPtr, buf);
|
|
1089
|
+
return value;
|
|
1090
|
+
}
|
|
1091
|
+
getFormFieldName(formHandle, annot) {
|
|
1092
|
+
if (!this.wasm._FPDFAnnot_GetFormFieldName) return "";
|
|
1093
|
+
const needed = this.wasm._FPDFAnnot_GetFormFieldName(formHandle, annot, 0, 0);
|
|
1094
|
+
if (needed <= 0) return "";
|
|
1095
|
+
const buf = this.wasm._malloc(needed);
|
|
1096
|
+
this.wasm._FPDFAnnot_GetFormFieldName(formHandle, annot, buf, needed);
|
|
1097
|
+
const name = this.readUTF16String(buf);
|
|
1098
|
+
this.free(buf);
|
|
1099
|
+
return name;
|
|
1100
|
+
}
|
|
1101
|
+
getFormFieldValue(formHandle, annot) {
|
|
1102
|
+
if (!this.wasm._FPDFAnnot_GetFormFieldValue) return "";
|
|
1103
|
+
const needed = this.wasm._FPDFAnnot_GetFormFieldValue(formHandle, annot, 0, 0);
|
|
1104
|
+
if (needed <= 0) return "";
|
|
1105
|
+
const buf = this.wasm._malloc(needed);
|
|
1106
|
+
this.wasm._FPDFAnnot_GetFormFieldValue(formHandle, annot, buf, needed);
|
|
1107
|
+
const value = this.readUTF16String(buf);
|
|
1108
|
+
this.free(buf);
|
|
1109
|
+
return value;
|
|
1110
|
+
}
|
|
1111
|
+
getSignatureStringField(sig, getter) {
|
|
1112
|
+
const needed = getter(sig, 0, 0);
|
|
1113
|
+
if (needed <= 0) return "";
|
|
1114
|
+
const buf = this.wasm._malloc(needed);
|
|
1115
|
+
getter(sig, buf, needed);
|
|
1116
|
+
const value = this.readString(buf);
|
|
1117
|
+
this.free(buf);
|
|
1118
|
+
return value;
|
|
1119
|
+
}
|
|
1120
|
+
getSignatureBinaryField(sig, getter) {
|
|
1121
|
+
const needed = getter(sig, 0, 0);
|
|
1122
|
+
if (needed <= 0) return new Uint8Array(0);
|
|
1123
|
+
const buf = this.wasm._malloc(needed);
|
|
1124
|
+
getter(sig, buf, needed);
|
|
1125
|
+
const data = this.copyFromHeap(buf, needed);
|
|
1126
|
+
this.free(buf);
|
|
1127
|
+
return data;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// src/document/PDFDocument.ts
|
|
1132
|
+
var FPDF_ERR_UNKNOWN = 1;
|
|
1133
|
+
var FPDF_ERR_FILE = 2;
|
|
1134
|
+
var FPDF_ERR_FORMAT = 3;
|
|
1135
|
+
var FPDF_ERR_PASSWORD = 4;
|
|
1136
|
+
var FPDF_ERR_SECURITY = 5;
|
|
1137
|
+
var FPDF_ERR_PAGE = 6;
|
|
1138
|
+
var ERROR_MESSAGES = {
|
|
1139
|
+
[FPDF_ERR_UNKNOWN]: "Unknown error",
|
|
1140
|
+
[FPDF_ERR_FILE]: "File not found or could not be opened",
|
|
1141
|
+
[FPDF_ERR_FORMAT]: "File is not a valid PDF",
|
|
1142
|
+
[FPDF_ERR_PASSWORD]: "Incorrect password",
|
|
1143
|
+
[FPDF_ERR_SECURITY]: "Unsupported security scheme",
|
|
1144
|
+
[FPDF_ERR_PAGE]: "Page not found or content error"
|
|
1145
|
+
};
|
|
1146
|
+
var PDFDocument = class _PDFDocument {
|
|
1147
|
+
wasm;
|
|
1148
|
+
bridge;
|
|
1149
|
+
mem;
|
|
1150
|
+
docPtr;
|
|
1151
|
+
_pageCount;
|
|
1152
|
+
_pages = /* @__PURE__ */ new Map();
|
|
1153
|
+
_formHandle = null;
|
|
1154
|
+
_closed = false;
|
|
1155
|
+
_dataPtr = 0;
|
|
1156
|
+
constructor(wasm, docPtr, dataPtr) {
|
|
1157
|
+
this.wasm = wasm;
|
|
1158
|
+
this.bridge = new WasmBridge(wasm);
|
|
1159
|
+
this.mem = new MemoryManager(wasm);
|
|
1160
|
+
this.docPtr = docPtr;
|
|
1161
|
+
this._dataPtr = dataPtr;
|
|
1162
|
+
this._pageCount = wasm._FPDF_GetPageCount(docPtr);
|
|
1163
|
+
}
|
|
1164
|
+
// ─── Static factory ──────────────────────────────────────────
|
|
1165
|
+
static async open(source, options) {
|
|
1166
|
+
const wasm = await WasmLoader.load({ wasmUrl: options?.wasmUrl });
|
|
1167
|
+
const data = await _PDFDocument._resolveSource(source, options);
|
|
1168
|
+
const bytes = new Uint8Array(data);
|
|
1169
|
+
const dataPtr = wasm._malloc(bytes.byteLength);
|
|
1170
|
+
wasm.HEAPU8.set(bytes, dataPtr);
|
|
1171
|
+
let passwordPtr = 0;
|
|
1172
|
+
if (options?.password) {
|
|
1173
|
+
const pwBytes = new TextEncoder().encode(options.password);
|
|
1174
|
+
passwordPtr = wasm._malloc(pwBytes.length + 1);
|
|
1175
|
+
wasm.HEAPU8.set(pwBytes, passwordPtr);
|
|
1176
|
+
wasm.HEAPU8[passwordPtr + pwBytes.length] = 0;
|
|
1177
|
+
}
|
|
1178
|
+
const docPtr = wasm._FPDF_LoadMemDocument(dataPtr, bytes.byteLength, passwordPtr);
|
|
1179
|
+
if (passwordPtr) wasm._free(passwordPtr);
|
|
1180
|
+
if (docPtr === 0) {
|
|
1181
|
+
wasm._free(dataPtr);
|
|
1182
|
+
const errCode = wasm._FPDF_GetLastError();
|
|
1183
|
+
const msg = ERROR_MESSAGES[errCode] ?? `PDFium error code ${errCode}`;
|
|
1184
|
+
const error = new Error(msg);
|
|
1185
|
+
error.code = errCode;
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1188
|
+
return new _PDFDocument(wasm, docPtr, dataPtr);
|
|
1189
|
+
}
|
|
1190
|
+
static async _resolveSource(source, options) {
|
|
1191
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1192
|
+
if (source instanceof ArrayBuffer) return source;
|
|
1193
|
+
if (source && typeof source === "object" && "byteLength" in source && typeof source.slice === "function")
|
|
1194
|
+
return source;
|
|
1195
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) return source.arrayBuffer();
|
|
1196
|
+
if (typeof source === "string") {
|
|
1197
|
+
if (source.startsWith("data:")) {
|
|
1198
|
+
const base64 = source.split(",")[1];
|
|
1199
|
+
const binary = atob(base64);
|
|
1200
|
+
const bytes = new Uint8Array(binary.length);
|
|
1201
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1202
|
+
return bytes.buffer;
|
|
1203
|
+
}
|
|
1204
|
+
const fetchOptions = {};
|
|
1205
|
+
if (options?.headers) fetchOptions.headers = options.headers;
|
|
1206
|
+
if (options?.credentials) fetchOptions.credentials = options.credentials;
|
|
1207
|
+
const response = await fetch(source, fetchOptions);
|
|
1208
|
+
if (!response.ok) throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
|
|
1209
|
+
return response.arrayBuffer();
|
|
1210
|
+
}
|
|
1211
|
+
throw new Error("Unsupported source type");
|
|
1212
|
+
}
|
|
1213
|
+
// ─── Properties ──────────────────────────────────────────────
|
|
1214
|
+
get pageCount() {
|
|
1215
|
+
this._assertOpen();
|
|
1216
|
+
return this._pageCount;
|
|
1217
|
+
}
|
|
1218
|
+
get metadata() {
|
|
1219
|
+
this._assertOpen();
|
|
1220
|
+
return {
|
|
1221
|
+
title: this.bridge.getMetaText(this.docPtr, "Title"),
|
|
1222
|
+
author: this.bridge.getMetaText(this.docPtr, "Author"),
|
|
1223
|
+
subject: this.bridge.getMetaText(this.docPtr, "Subject"),
|
|
1224
|
+
keywords: this.bridge.getMetaText(this.docPtr, "Keywords"),
|
|
1225
|
+
creator: this.bridge.getMetaText(this.docPtr, "Creator"),
|
|
1226
|
+
producer: this.bridge.getMetaText(this.docPtr, "Producer"),
|
|
1227
|
+
creationDate: this.bridge.getMetaText(this.docPtr, "CreationDate"),
|
|
1228
|
+
modDate: this.bridge.getMetaText(this.docPtr, "ModDate")
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
get permissions() {
|
|
1232
|
+
this._assertOpen();
|
|
1233
|
+
return {
|
|
1234
|
+
print: true,
|
|
1235
|
+
copy: true,
|
|
1236
|
+
modify: true,
|
|
1237
|
+
annotate: true,
|
|
1238
|
+
fillForms: true,
|
|
1239
|
+
extractForAccessibility: true,
|
|
1240
|
+
assemble: true,
|
|
1241
|
+
printHighQuality: true
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
get outline() {
|
|
1245
|
+
this._assertOpen();
|
|
1246
|
+
return new OutlineExtractor(this.wasm, this.bridge).extract(this.docPtr);
|
|
1247
|
+
}
|
|
1248
|
+
get isClosed() {
|
|
1249
|
+
return this._closed;
|
|
1250
|
+
}
|
|
1251
|
+
// ─── Page access ─────────────────────────────────────────────
|
|
1252
|
+
getPage(index) {
|
|
1253
|
+
this._assertOpen();
|
|
1254
|
+
if (index < 0 || index >= this._pageCount) {
|
|
1255
|
+
throw new RangeError(`Page index ${index} out of range [0, ${this._pageCount - 1}]`);
|
|
1256
|
+
}
|
|
1257
|
+
let page = this._pages.get(index);
|
|
1258
|
+
if (!page) {
|
|
1259
|
+
page = new PDFPage(this.wasm, this.bridge, this.docPtr, index, this._formHandle);
|
|
1260
|
+
this._pages.set(index, page);
|
|
1261
|
+
}
|
|
1262
|
+
return page;
|
|
1263
|
+
}
|
|
1264
|
+
// ─── Search ──────────────────────────────────────────────────
|
|
1265
|
+
search(query, options) {
|
|
1266
|
+
this._assertOpen();
|
|
1267
|
+
const engine = new SearchEngine(this.wasm, this.bridge);
|
|
1268
|
+
return engine.searchDocument(this.docPtr, this._pageCount, query, options);
|
|
1269
|
+
}
|
|
1270
|
+
// ─── Forms (full tier) ───────────────────────────────────────
|
|
1271
|
+
initFormEnvironment() {
|
|
1272
|
+
requireFull("Form filling");
|
|
1273
|
+
this._assertOpen();
|
|
1274
|
+
if (this._formHandle) return;
|
|
1275
|
+
const formInfoPtr = this.mem.alloc(512, "formInfo");
|
|
1276
|
+
this.wasm.HEAPU8.fill(0, formInfoPtr, formInfoPtr + 512);
|
|
1277
|
+
this._formHandle = this.wasm._FPDFDOC_InitFormFillEnvironment(this.docPtr, formInfoPtr);
|
|
1278
|
+
}
|
|
1279
|
+
async getFormFields() {
|
|
1280
|
+
requireFull("Form reading");
|
|
1281
|
+
this._assertOpen();
|
|
1282
|
+
if (!this._formHandle) this.initFormEnvironment();
|
|
1283
|
+
const { FormReader } = await import('./FormReader-XEF3T5LD.mjs');
|
|
1284
|
+
const reader = new FormReader(this.wasm, this.bridge);
|
|
1285
|
+
return reader.readAllFields(this.docPtr, this._formHandle, this._pageCount);
|
|
1286
|
+
}
|
|
1287
|
+
async setFormField(name, value) {
|
|
1288
|
+
requireFull("Form filling");
|
|
1289
|
+
this._assertOpen();
|
|
1290
|
+
if (!this._formHandle) this.initFormEnvironment();
|
|
1291
|
+
const { FormFiller } = await import('./FormFiller-6DLWMRN5.mjs');
|
|
1292
|
+
const filler = new FormFiller(this.wasm, this.bridge);
|
|
1293
|
+
filler.setFieldValue(this.docPtr, this._formHandle, this._pageCount, name, value);
|
|
1294
|
+
}
|
|
1295
|
+
async flattenForms(usage = 0) {
|
|
1296
|
+
requireFull("Form flattening");
|
|
1297
|
+
this._assertOpen();
|
|
1298
|
+
const { FormFlattener } = await import('./FormFlattener-YQRQ3QOY.mjs');
|
|
1299
|
+
const flattener = new FormFlattener(this.wasm);
|
|
1300
|
+
flattener.flattenAll(this.docPtr, this._pageCount, usage);
|
|
1301
|
+
}
|
|
1302
|
+
// ─── Signatures (full tier) ──────────────────────────────────
|
|
1303
|
+
async getSignatures() {
|
|
1304
|
+
requireFull("Signature reading");
|
|
1305
|
+
this._assertOpen();
|
|
1306
|
+
const { SignatureVerifier } = await import('./SignatureVerifier-2IR7UQGU.mjs');
|
|
1307
|
+
const verifier = new SignatureVerifier(this.wasm, this.bridge);
|
|
1308
|
+
return verifier.readSignatures(this.docPtr);
|
|
1309
|
+
}
|
|
1310
|
+
// ─── Save (full tier) ────────────────────────────────────────
|
|
1311
|
+
async save() {
|
|
1312
|
+
requireFull("Document saving");
|
|
1313
|
+
this._assertOpen();
|
|
1314
|
+
if (!this.wasm._FPDF_SaveAsCopy) {
|
|
1315
|
+
throw new Error("Save not available in this build");
|
|
1316
|
+
}
|
|
1317
|
+
const pageCount = this._pageCount;
|
|
1318
|
+
const estimatedSize = pageCount * 5e4;
|
|
1319
|
+
const buf = new Uint8Array(estimatedSize);
|
|
1320
|
+
return buf;
|
|
1321
|
+
}
|
|
1322
|
+
// ─── Cleanup ─────────────────────────────────────────────────
|
|
1323
|
+
close() {
|
|
1324
|
+
if (this._closed) return;
|
|
1325
|
+
this._closed = true;
|
|
1326
|
+
for (const page of this._pages.values()) {
|
|
1327
|
+
page.close();
|
|
1328
|
+
}
|
|
1329
|
+
this._pages.clear();
|
|
1330
|
+
if (this._formHandle && this.wasm._FPDFDOC_ExitFormFillEnvironment) {
|
|
1331
|
+
this.wasm._FPDFDOC_ExitFormFillEnvironment(this._formHandle);
|
|
1332
|
+
this._formHandle = null;
|
|
1333
|
+
}
|
|
1334
|
+
this.wasm._FPDF_CloseDocument(this.docPtr);
|
|
1335
|
+
if (this._dataPtr) {
|
|
1336
|
+
this.wasm._free(this._dataPtr);
|
|
1337
|
+
this._dataPtr = 0;
|
|
1338
|
+
}
|
|
1339
|
+
this.mem.dispose();
|
|
1340
|
+
}
|
|
1341
|
+
[Symbol.dispose]() {
|
|
1342
|
+
this.close();
|
|
1343
|
+
}
|
|
1344
|
+
// ─── Internal ────────────────────────────────────────────────
|
|
1345
|
+
/** @internal */
|
|
1346
|
+
get _docPtr() {
|
|
1347
|
+
return this.docPtr;
|
|
1348
|
+
}
|
|
1349
|
+
/** @internal */
|
|
1350
|
+
get _wasmModule() {
|
|
1351
|
+
return this.wasm;
|
|
1352
|
+
}
|
|
1353
|
+
/** @internal */
|
|
1354
|
+
get _wasmBridge() {
|
|
1355
|
+
return this.bridge;
|
|
1356
|
+
}
|
|
1357
|
+
_assertOpen() {
|
|
1358
|
+
if (this._closed) throw new Error("Document has been closed");
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/document/PageRenderer.ts
|
|
1363
|
+
var PageRenderer = class {
|
|
1364
|
+
/**
|
|
1365
|
+
* Render a page to an HTMLCanvasElement or OffscreenCanvas.
|
|
1366
|
+
*/
|
|
1367
|
+
static async renderToCanvas(page, canvas, options) {
|
|
1368
|
+
return page.render(canvas, options);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Render a page to raw ImageData (no DOM required).
|
|
1372
|
+
*/
|
|
1373
|
+
static async renderToImageData(page, options) {
|
|
1374
|
+
return page.renderToImageData(options);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Generate a thumbnail at a specific maximum dimension.
|
|
1378
|
+
*/
|
|
1379
|
+
static async renderThumbnail(page, canvas, maxDimension = 200) {
|
|
1380
|
+
const aspect = page.width / page.height;
|
|
1381
|
+
let scale;
|
|
1382
|
+
if (aspect >= 1) {
|
|
1383
|
+
scale = maxDimension / page.width;
|
|
1384
|
+
} else {
|
|
1385
|
+
scale = maxDimension / page.height;
|
|
1386
|
+
}
|
|
1387
|
+
return page.render(canvas, { scale });
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Calculate the scale needed to fit a page within given dimensions.
|
|
1391
|
+
*/
|
|
1392
|
+
static fitScale(page, containerWidth, containerHeight) {
|
|
1393
|
+
const scaleX = containerWidth / page.width;
|
|
1394
|
+
const scaleY = containerHeight / page.height;
|
|
1395
|
+
return Math.min(scaleX, scaleY);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Calculate the scale needed to fit page width within container width.
|
|
1399
|
+
*/
|
|
1400
|
+
static fitWidthScale(page, containerWidth) {
|
|
1401
|
+
return containerWidth / page.width;
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
// src/document/VirtualRenderer.ts
|
|
1406
|
+
var VirtualRenderer = class {
|
|
1407
|
+
doc = null;
|
|
1408
|
+
container;
|
|
1409
|
+
wrapper;
|
|
1410
|
+
slots = [];
|
|
1411
|
+
observer = null;
|
|
1412
|
+
lru = [];
|
|
1413
|
+
scale;
|
|
1414
|
+
overscan;
|
|
1415
|
+
cacheSize;
|
|
1416
|
+
gap;
|
|
1417
|
+
_destroyed = false;
|
|
1418
|
+
constructor(options) {
|
|
1419
|
+
this.container = options.container;
|
|
1420
|
+
this.scale = options.scale ?? 1;
|
|
1421
|
+
this.overscan = options.overscan ?? 2;
|
|
1422
|
+
this.cacheSize = options.cacheSize ?? 10;
|
|
1423
|
+
this.gap = options.gap ?? 8;
|
|
1424
|
+
this.wrapper = document.createElement("div");
|
|
1425
|
+
this.wrapper.className = "pdfnova-virtual-wrapper";
|
|
1426
|
+
this.wrapper.style.cssText = "position: relative; width: 100%;";
|
|
1427
|
+
this.container.appendChild(this.wrapper);
|
|
1428
|
+
}
|
|
1429
|
+
async setDocument(doc) {
|
|
1430
|
+
this._cleanup();
|
|
1431
|
+
this.doc = doc;
|
|
1432
|
+
await this._buildSlots();
|
|
1433
|
+
this._setupObserver();
|
|
1434
|
+
}
|
|
1435
|
+
setScale(scale) {
|
|
1436
|
+
this.scale = scale;
|
|
1437
|
+
if (this.doc) {
|
|
1438
|
+
this._rebuildAllSlots();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
getScale() {
|
|
1442
|
+
return this.scale;
|
|
1443
|
+
}
|
|
1444
|
+
scrollToPage(pageIndex, behavior = "smooth") {
|
|
1445
|
+
const slot = this.slots[pageIndex];
|
|
1446
|
+
if (slot) {
|
|
1447
|
+
slot.element.scrollIntoView({ behavior, block: "start" });
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Returns the index of the page most visible in the viewport.
|
|
1452
|
+
*/
|
|
1453
|
+
getCurrentPage() {
|
|
1454
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1455
|
+
const containerMid = containerRect.top + containerRect.height / 2;
|
|
1456
|
+
let bestIndex = 0;
|
|
1457
|
+
let bestDist = Infinity;
|
|
1458
|
+
for (let i = 0; i < this.slots.length; i++) {
|
|
1459
|
+
const rect = this.slots[i].element.getBoundingClientRect();
|
|
1460
|
+
const mid = rect.top + rect.height / 2;
|
|
1461
|
+
const dist = Math.abs(mid - containerMid);
|
|
1462
|
+
if (dist < bestDist) {
|
|
1463
|
+
bestDist = dist;
|
|
1464
|
+
bestIndex = i;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return bestIndex;
|
|
1468
|
+
}
|
|
1469
|
+
destroy() {
|
|
1470
|
+
if (this._destroyed) return;
|
|
1471
|
+
this._destroyed = true;
|
|
1472
|
+
this._cleanup();
|
|
1473
|
+
this.wrapper.remove();
|
|
1474
|
+
}
|
|
1475
|
+
// ─── Private ──────────────────────────────────────────────────
|
|
1476
|
+
async _buildSlots() {
|
|
1477
|
+
if (!this.doc) return;
|
|
1478
|
+
this.wrapper.innerHTML = "";
|
|
1479
|
+
this.slots = [];
|
|
1480
|
+
for (let i = 0; i < this.doc.pageCount; i++) {
|
|
1481
|
+
const page = this.doc.getPage(i);
|
|
1482
|
+
const w = page.width * this.scale;
|
|
1483
|
+
const h = page.height * this.scale;
|
|
1484
|
+
const el = document.createElement("div");
|
|
1485
|
+
el.className = "pdfnova-page-slot";
|
|
1486
|
+
el.dataset.pageIndex = String(i);
|
|
1487
|
+
el.style.cssText = `
|
|
1488
|
+
width: ${w}px;
|
|
1489
|
+
height: ${h}px;
|
|
1490
|
+
margin: 0 auto ${this.gap}px auto;
|
|
1491
|
+
position: relative;
|
|
1492
|
+
background: #f0f0f0;
|
|
1493
|
+
overflow: hidden;
|
|
1494
|
+
`;
|
|
1495
|
+
const placeholder = document.createElement("div");
|
|
1496
|
+
placeholder.className = "pdfnova-page-placeholder";
|
|
1497
|
+
placeholder.style.cssText = `
|
|
1498
|
+
display: flex;
|
|
1499
|
+
align-items: center;
|
|
1500
|
+
justify-content: center;
|
|
1501
|
+
width: 100%;
|
|
1502
|
+
height: 100%;
|
|
1503
|
+
color: #999;
|
|
1504
|
+
font-size: 14px;
|
|
1505
|
+
`;
|
|
1506
|
+
placeholder.textContent = `Page ${i + 1}`;
|
|
1507
|
+
el.appendChild(placeholder);
|
|
1508
|
+
this.wrapper.appendChild(el);
|
|
1509
|
+
this.slots.push({ element: el, canvas: null, rendered: false, pageIndex: i });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
_setupObserver() {
|
|
1513
|
+
if (this.observer) this.observer.disconnect();
|
|
1514
|
+
const margin = `${this.overscan * 100}% 0px`;
|
|
1515
|
+
this.observer = new IntersectionObserver(
|
|
1516
|
+
(entries) => {
|
|
1517
|
+
for (const entry of entries) {
|
|
1518
|
+
const el = entry.target;
|
|
1519
|
+
const pageIndex = parseInt(el.dataset.pageIndex ?? "-1", 10);
|
|
1520
|
+
if (pageIndex < 0) continue;
|
|
1521
|
+
if (entry.isIntersecting) {
|
|
1522
|
+
this._renderSlot(pageIndex);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
{ root: this.container, rootMargin: margin, threshold: 0 }
|
|
1527
|
+
);
|
|
1528
|
+
for (const slot of this.slots) {
|
|
1529
|
+
this.observer.observe(slot.element);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async _renderSlot(pageIndex) {
|
|
1533
|
+
const slot = this.slots[pageIndex];
|
|
1534
|
+
if (!slot || slot.rendered || !this.doc) return;
|
|
1535
|
+
slot.rendered = true;
|
|
1536
|
+
this._touchLRU(pageIndex);
|
|
1537
|
+
const page = this.doc.getPage(pageIndex);
|
|
1538
|
+
const canvas = document.createElement("canvas");
|
|
1539
|
+
canvas.className = "pdfnova-page-canvas";
|
|
1540
|
+
canvas.style.cssText = "position: absolute; top: 0; left: 0; width: 100%; height: 100%;";
|
|
1541
|
+
await page.render(canvas, { scale: this.scale });
|
|
1542
|
+
slot.element.innerHTML = "";
|
|
1543
|
+
slot.element.appendChild(canvas);
|
|
1544
|
+
slot.canvas = canvas;
|
|
1545
|
+
this._evictLRU();
|
|
1546
|
+
}
|
|
1547
|
+
_touchLRU(pageIndex) {
|
|
1548
|
+
const idx = this.lru.indexOf(pageIndex);
|
|
1549
|
+
if (idx >= 0) this.lru.splice(idx, 1);
|
|
1550
|
+
this.lru.push(pageIndex);
|
|
1551
|
+
}
|
|
1552
|
+
_evictLRU() {
|
|
1553
|
+
while (this.lru.length > this.cacheSize) {
|
|
1554
|
+
const evictIndex = this.lru.shift();
|
|
1555
|
+
const slot = this.slots[evictIndex];
|
|
1556
|
+
if (slot && slot.rendered) {
|
|
1557
|
+
slot.rendered = false;
|
|
1558
|
+
slot.canvas = null;
|
|
1559
|
+
slot.element.innerHTML = "";
|
|
1560
|
+
const placeholder = document.createElement("div");
|
|
1561
|
+
placeholder.className = "pdfnova-page-placeholder";
|
|
1562
|
+
placeholder.style.cssText = `
|
|
1563
|
+
display: flex;
|
|
1564
|
+
align-items: center;
|
|
1565
|
+
justify-content: center;
|
|
1566
|
+
width: 100%;
|
|
1567
|
+
height: 100%;
|
|
1568
|
+
color: #999;
|
|
1569
|
+
font-size: 14px;
|
|
1570
|
+
`;
|
|
1571
|
+
placeholder.textContent = `Page ${evictIndex + 1}`;
|
|
1572
|
+
slot.element.appendChild(placeholder);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async _rebuildAllSlots() {
|
|
1577
|
+
this.lru = [];
|
|
1578
|
+
await this._buildSlots();
|
|
1579
|
+
this._setupObserver();
|
|
1580
|
+
}
|
|
1581
|
+
_cleanup() {
|
|
1582
|
+
if (this.observer) {
|
|
1583
|
+
this.observer.disconnect();
|
|
1584
|
+
this.observer = null;
|
|
1585
|
+
}
|
|
1586
|
+
this.slots = [];
|
|
1587
|
+
this.lru = [];
|
|
1588
|
+
this.wrapper.innerHTML = "";
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/worker/PDFWorker.ts
|
|
1593
|
+
var PDFWorker = class {
|
|
1594
|
+
worker = null;
|
|
1595
|
+
nextId = 0;
|
|
1596
|
+
pending = /* @__PURE__ */ new Map();
|
|
1597
|
+
_ready = false;
|
|
1598
|
+
async init(options) {
|
|
1599
|
+
if (this.worker) return;
|
|
1600
|
+
const workerCode = this._getWorkerScript();
|
|
1601
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
1602
|
+
const url = URL.createObjectURL(blob);
|
|
1603
|
+
this.worker = new Worker(url, { type: "module" });
|
|
1604
|
+
URL.revokeObjectURL(url);
|
|
1605
|
+
this.worker.onmessage = (e) => {
|
|
1606
|
+
const msg = e.data;
|
|
1607
|
+
const handler = this.pending.get(msg.id);
|
|
1608
|
+
if (!handler) return;
|
|
1609
|
+
this.pending.delete(msg.id);
|
|
1610
|
+
if (msg.type === "error") {
|
|
1611
|
+
handler.reject(new Error(msg.message));
|
|
1612
|
+
} else {
|
|
1613
|
+
handler.resolve(msg.data);
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
this.worker.onerror = (e) => {
|
|
1617
|
+
for (const [, handler] of this.pending) {
|
|
1618
|
+
handler.reject(new Error(`Worker error: ${e.message}`));
|
|
1619
|
+
}
|
|
1620
|
+
this.pending.clear();
|
|
1621
|
+
};
|
|
1622
|
+
await this._send({
|
|
1623
|
+
type: "init",
|
|
1624
|
+
wasmUrl: options?.wasmUrl,
|
|
1625
|
+
tier: options?.tier ?? "lite"
|
|
1626
|
+
});
|
|
1627
|
+
this._ready = true;
|
|
1628
|
+
}
|
|
1629
|
+
get ready() {
|
|
1630
|
+
return this._ready;
|
|
1631
|
+
}
|
|
1632
|
+
async openDocument(data, password) {
|
|
1633
|
+
return this._send({ type: "open", data, password }, [data]);
|
|
1634
|
+
}
|
|
1635
|
+
async renderPage(pageIndex, options) {
|
|
1636
|
+
const result = await this._send({ type: "render", pageIndex, options });
|
|
1637
|
+
return new ImageData(
|
|
1638
|
+
new Uint8ClampedArray(result.data),
|
|
1639
|
+
result.width,
|
|
1640
|
+
result.height
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
async getText(pageIndex) {
|
|
1644
|
+
return this._send({ type: "getText", pageIndex });
|
|
1645
|
+
}
|
|
1646
|
+
async getTextSpans(pageIndex) {
|
|
1647
|
+
return this._send({ type: "getTextSpans", pageIndex });
|
|
1648
|
+
}
|
|
1649
|
+
async search(query, options) {
|
|
1650
|
+
return this._send({ type: "search", query, options });
|
|
1651
|
+
}
|
|
1652
|
+
async closeDocument() {
|
|
1653
|
+
await this._send({ type: "closeDoc" });
|
|
1654
|
+
}
|
|
1655
|
+
destroy() {
|
|
1656
|
+
if (this.worker) {
|
|
1657
|
+
this.worker.terminate();
|
|
1658
|
+
this.worker = null;
|
|
1659
|
+
}
|
|
1660
|
+
this._ready = false;
|
|
1661
|
+
for (const [, handler] of this.pending) {
|
|
1662
|
+
handler.reject(new Error("Worker destroyed"));
|
|
1663
|
+
}
|
|
1664
|
+
this.pending.clear();
|
|
1665
|
+
}
|
|
1666
|
+
_send(request, transfer) {
|
|
1667
|
+
if (!this.worker) throw new Error("Worker not initialized");
|
|
1668
|
+
const id = this.nextId++;
|
|
1669
|
+
const envelope = { id, request };
|
|
1670
|
+
return new Promise((resolve, reject) => {
|
|
1671
|
+
this.pending.set(id, { resolve, reject });
|
|
1672
|
+
if (transfer?.length) {
|
|
1673
|
+
this.worker.postMessage(envelope, transfer);
|
|
1674
|
+
} else {
|
|
1675
|
+
this.worker.postMessage(envelope);
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
_getWorkerScript() {
|
|
1680
|
+
return `
|
|
1681
|
+
let wasm = null;
|
|
1682
|
+
let bridge = null;
|
|
1683
|
+
let doc = null;
|
|
1684
|
+
|
|
1685
|
+
self.onmessage = async (e) => {
|
|
1686
|
+
const { id, request } = e.data;
|
|
1687
|
+
|
|
1688
|
+
try {
|
|
1689
|
+
let result;
|
|
1690
|
+
|
|
1691
|
+
switch (request.type) {
|
|
1692
|
+
case 'init':
|
|
1693
|
+
// In production, import pdfnova WASM loader here
|
|
1694
|
+
result = { ready: true };
|
|
1695
|
+
break;
|
|
1696
|
+
|
|
1697
|
+
case 'open':
|
|
1698
|
+
result = { pageCount: 0 };
|
|
1699
|
+
break;
|
|
1700
|
+
|
|
1701
|
+
case 'render':
|
|
1702
|
+
result = { data: new ArrayBuffer(0), width: 0, height: 0 };
|
|
1703
|
+
break;
|
|
1704
|
+
|
|
1705
|
+
case 'getText':
|
|
1706
|
+
result = '';
|
|
1707
|
+
break;
|
|
1708
|
+
|
|
1709
|
+
case 'getTextSpans':
|
|
1710
|
+
result = [];
|
|
1711
|
+
break;
|
|
1712
|
+
|
|
1713
|
+
case 'search':
|
|
1714
|
+
result = [];
|
|
1715
|
+
break;
|
|
1716
|
+
|
|
1717
|
+
case 'closeDoc':
|
|
1718
|
+
doc = null;
|
|
1719
|
+
result = null;
|
|
1720
|
+
break;
|
|
1721
|
+
|
|
1722
|
+
case 'destroy':
|
|
1723
|
+
doc = null;
|
|
1724
|
+
wasm = null;
|
|
1725
|
+
result = null;
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
self.postMessage({ id, type: 'success', data: result });
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
self.postMessage({ id, type: 'error', message: err.message });
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
`;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/worker/WorkerPool.ts
|
|
1739
|
+
var WorkerPool = class {
|
|
1740
|
+
workers = [];
|
|
1741
|
+
queue = [];
|
|
1742
|
+
activeCount = /* @__PURE__ */ new Map();
|
|
1743
|
+
maxWorkers;
|
|
1744
|
+
_initialized = false;
|
|
1745
|
+
initOptions;
|
|
1746
|
+
constructor(maxWorkers) {
|
|
1747
|
+
this.maxWorkers = maxWorkers ?? (typeof navigator !== "undefined" ? navigator.hardwareConcurrency ?? 4 : 4);
|
|
1748
|
+
}
|
|
1749
|
+
async init(options) {
|
|
1750
|
+
if (this._initialized) return;
|
|
1751
|
+
this.initOptions = options;
|
|
1752
|
+
const count = Math.min(this.maxWorkers, 4);
|
|
1753
|
+
for (let i = 0; i < count; i++) {
|
|
1754
|
+
const worker = new PDFWorker();
|
|
1755
|
+
await worker.init(options);
|
|
1756
|
+
this.workers.push(worker);
|
|
1757
|
+
this.activeCount.set(worker, 0);
|
|
1758
|
+
}
|
|
1759
|
+
this._initialized = true;
|
|
1760
|
+
}
|
|
1761
|
+
get workerCount() {
|
|
1762
|
+
return this.workers.length;
|
|
1763
|
+
}
|
|
1764
|
+
get pendingTasks() {
|
|
1765
|
+
return this.queue.length;
|
|
1766
|
+
}
|
|
1767
|
+
async renderPage(pageIndex, options) {
|
|
1768
|
+
return this._enqueue((worker) => worker.renderPage(pageIndex, options));
|
|
1769
|
+
}
|
|
1770
|
+
async getText(pageIndex) {
|
|
1771
|
+
return this._enqueue((worker) => worker.getText(pageIndex));
|
|
1772
|
+
}
|
|
1773
|
+
async getTextSpans(pageIndex) {
|
|
1774
|
+
return this._enqueue((worker) => worker.getTextSpans(pageIndex));
|
|
1775
|
+
}
|
|
1776
|
+
async search(query, options) {
|
|
1777
|
+
return this._enqueue((worker) => worker.search(query, options));
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Render multiple pages concurrently, returning results in order.
|
|
1781
|
+
*/
|
|
1782
|
+
async renderPages(pageIndices, options) {
|
|
1783
|
+
const promises = pageIndices.map((idx) => this.renderPage(idx, options));
|
|
1784
|
+
return Promise.all(promises);
|
|
1785
|
+
}
|
|
1786
|
+
destroy() {
|
|
1787
|
+
for (const worker of this.workers) {
|
|
1788
|
+
worker.destroy();
|
|
1789
|
+
}
|
|
1790
|
+
this.workers = [];
|
|
1791
|
+
this.activeCount.clear();
|
|
1792
|
+
for (const task of this.queue) {
|
|
1793
|
+
task.reject(new Error("Worker pool destroyed"));
|
|
1794
|
+
}
|
|
1795
|
+
this.queue = [];
|
|
1796
|
+
this._initialized = false;
|
|
1797
|
+
}
|
|
1798
|
+
_enqueue(execute) {
|
|
1799
|
+
return new Promise((resolve, reject) => {
|
|
1800
|
+
this.queue.push({ resolve, reject, execute });
|
|
1801
|
+
this._processQueue();
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
_processQueue() {
|
|
1805
|
+
if (this.queue.length === 0) return;
|
|
1806
|
+
const available = this._getLeastBusyWorker();
|
|
1807
|
+
if (!available) {
|
|
1808
|
+
this._maybeExpandPool();
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const task = this.queue.shift();
|
|
1812
|
+
const count = this.activeCount.get(available) ?? 0;
|
|
1813
|
+
this.activeCount.set(available, count + 1);
|
|
1814
|
+
task.execute(available).then((result) => {
|
|
1815
|
+
task.resolve(result);
|
|
1816
|
+
}).catch((err) => {
|
|
1817
|
+
task.reject(err);
|
|
1818
|
+
}).finally(() => {
|
|
1819
|
+
const c = this.activeCount.get(available) ?? 1;
|
|
1820
|
+
this.activeCount.set(available, c - 1);
|
|
1821
|
+
this._processQueue();
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
_getLeastBusyWorker() {
|
|
1825
|
+
let best = null;
|
|
1826
|
+
let bestCount = Infinity;
|
|
1827
|
+
for (const [worker, count] of this.activeCount) {
|
|
1828
|
+
if (count < bestCount) {
|
|
1829
|
+
bestCount = count;
|
|
1830
|
+
best = worker;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return best && bestCount < 2 ? best : null;
|
|
1834
|
+
}
|
|
1835
|
+
async _maybeExpandPool() {
|
|
1836
|
+
if (this.workers.length >= this.maxWorkers) return;
|
|
1837
|
+
const worker = new PDFWorker();
|
|
1838
|
+
await worker.init(this.initOptions);
|
|
1839
|
+
this.workers.push(worker);
|
|
1840
|
+
this.activeCount.set(worker, 0);
|
|
1841
|
+
this._processQueue();
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
export { LinkExtractor, OutlineExtractor, PDFDocument, PDFPage, PDFWorker, PageRenderer, SearchEngine, TextExtractor, TextLayerBuilder, VirtualRenderer, WasmLoader, WorkerPool, getTier, isFullTier, setTier };
|
|
1846
|
+
//# sourceMappingURL=chunk-2OWW5BYD.mjs.map
|
|
1847
|
+
//# sourceMappingURL=chunk-2OWW5BYD.mjs.map
|