web-doc 0.6.2 → 0.8.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/THIRD_PARTY_NOTICES.md +33 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +53 -2
- package/dist/edit/ai/outline.d.ts +47 -0
- package/dist/edit/ai/outline.js +338 -0
- package/dist/edit/ai/targets.d.ts +16 -0
- package/dist/edit/ai/targets.js +309 -0
- package/dist/edit/ai/tools.d.ts +28 -0
- package/dist/edit/ai/tools.js +605 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +57 -0
- package/dist/edit/docx/engine.js +547 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +94 -0
- package/dist/edit/docx/model.js +350 -0
- package/dist/edit/docx/operations.d.ts +47 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +16 -0
- package/dist/edit/docx/provider.js +37 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +48 -0
- package/dist/edit/docx/session.js +467 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +529 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +241 -0
- package/dist/edit/docx/text-ops.d.ts +40 -0
- package/dist/edit/docx/text-ops.js +491 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +284 -0
- package/dist/edit/docx/tracked.d.ts +52 -0
- package/dist/edit/docx/tracked.js +347 -0
- package/dist/edit/docx/types.d.ts +206 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +64 -0
- package/dist/edit/docx/write.js +375 -0
- package/dist/edit/engine.d.ts +128 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +77 -0
- package/dist/edit/history.js +123 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +16 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +63 -0
- package/dist/edit/pdf/session.js +312 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +99 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +54 -0
- package/dist/edit/pptx/session.js +145 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +77 -0
- package/dist/edit/session.js +943 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +290 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +5 -1
- package/dist/headless.js +8 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +16 -2
- package/dist/limits.js +6 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +9212 -0
- package/dist/workers/pdf-edit-worker.js +10847 -0
- package/package.json +3 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createPdfEditHandler } from "./edit/pdf/engine/handler.js";
|
|
2
|
+
import { Pdfium } from "./edit/pdf/engine/pdfium.js";
|
|
3
|
+
import { attachWorkerEndpoint } from "./worker-endpoint.js";
|
|
4
|
+
/*
|
|
5
|
+
* Module worker that owns a PDFium instance for one edit session. The WASM is
|
|
6
|
+
* fetched from the URL the main thread resolved, so hosting the assets
|
|
7
|
+
* elsewhere (assetBaseUrl) needs no change here.
|
|
8
|
+
*/
|
|
9
|
+
async function fetchBytes(url) {
|
|
10
|
+
const response = await fetch(url);
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
throw new Error(`Fetching ${url} failed with HTTP ${response.status}`);
|
|
13
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
14
|
+
}
|
|
15
|
+
attachWorkerEndpoint(self, createPdfEditHandler({
|
|
16
|
+
loadPdfium: async (wasmUrl) => Pdfium.load(await fetchBytes(wasmUrl)),
|
|
17
|
+
fetchBytes,
|
|
18
|
+
decodeImage: async (bytes, mimeType) => {
|
|
19
|
+
const bitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: mimeType }));
|
|
20
|
+
try {
|
|
21
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
22
|
+
const context = canvas.getContext("2d");
|
|
23
|
+
context.drawImage(bitmap, 0, 0);
|
|
24
|
+
const { data } = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
25
|
+
return {
|
|
26
|
+
width: bitmap.width,
|
|
27
|
+
height: bitmap.height,
|
|
28
|
+
rgba: new Uint8Array(data.buffer),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
bitmap.close();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
@@ -22,6 +22,11 @@ export declare class SpreadsheetViewport {
|
|
|
22
22
|
#private;
|
|
23
23
|
constructor(container: HTMLElement, host: ViewportHost);
|
|
24
24
|
setDocument(info: DocumentInfo | undefined): void;
|
|
25
|
+
/** Sheets are not editable yet, so new content is treated as a new document. */
|
|
26
|
+
replaceDocument(info: DocumentInfo, _changedPages?: readonly number[], _revision?: number): void;
|
|
27
|
+
/** Sheet geometry is cell-addressed; page-space helpers do not apply. */
|
|
28
|
+
pageToClient(): undefined;
|
|
29
|
+
clientToPage(): undefined;
|
|
25
30
|
update(): void;
|
|
26
31
|
panBy(deltaX: number, deltaY: number): void;
|
|
27
32
|
/** Sheet matches are cell-addressed; the sheet viewport scrolls per cell already. */
|
|
@@ -203,6 +203,17 @@ export class SpreadsheetViewport {
|
|
|
203
203
|
this.#root.scrollTo({ left: 0, top: 0 });
|
|
204
204
|
this.schedule();
|
|
205
205
|
}
|
|
206
|
+
/** Sheets are not editable yet, so new content is treated as a new document. */
|
|
207
|
+
replaceDocument(info, _changedPages, _revision) {
|
|
208
|
+
this.setDocument(info);
|
|
209
|
+
}
|
|
210
|
+
/** Sheet geometry is cell-addressed; page-space helpers do not apply. */
|
|
211
|
+
pageToClient() {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
clientToPage() {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
206
217
|
update() {
|
|
207
218
|
if (!this.#info)
|
|
208
219
|
return;
|
package/dist/ui.js
CHANGED
|
@@ -120,6 +120,13 @@ export class BasicViewerUi {
|
|
|
120
120
|
if (!this.#thumbnailPanel.hidden)
|
|
121
121
|
void this.#renderThumbnails();
|
|
122
122
|
this.#renderSheetTabs();
|
|
123
|
+
}), viewer.on("documentchange", () => {
|
|
124
|
+
// Edited content: counters, tabs and thumbnails follow the new document.
|
|
125
|
+
this.#info = viewer.getDocumentInfo();
|
|
126
|
+
this.#renderSheetTabs();
|
|
127
|
+
this.#syncState(viewer.state);
|
|
128
|
+
if (!this.#thumbnailPanel.hidden)
|
|
129
|
+
void this.#renderThumbnails();
|
|
123
130
|
}));
|
|
124
131
|
this.root.addEventListener("keydown", this.#onKeyDown);
|
|
125
132
|
document.addEventListener("fullscreenchange", this.#onFullscreenChange);
|
package/dist/viewer.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CellRange, CellSelection, DocumentInfo, DocumentSource, FitMode, HeadlessRenderOptions, OpenDocumentOptions, ResourceLimits, SearchOptions, SearchResult, SpreadsheetViewportRange, TextSelection, TextSelectionRange, ThumbnailRenderOptions, ViewerApi, ViewerEventListener, ViewerEventMap, ViewerFetch, ViewerLogger, ViewerOptions, ViewerState } from "./contracts.js";
|
|
2
|
+
import type { EditOptions, PageHit, PageRect, ViewportRect } from "./edit/types.js";
|
|
3
|
+
import type { EditSession } from "./edit/sessions.js";
|
|
2
4
|
import type { AdapterRegistry } from "./registry.js";
|
|
3
5
|
import type { FontManager } from "./fonts.js";
|
|
4
6
|
import { RenderScheduler } from "./render-scheduler.js";
|
|
@@ -47,6 +49,10 @@ export declare class DocumentViewer implements ViewerApi {
|
|
|
47
49
|
copySelection(): Promise<string>;
|
|
48
50
|
getOriginalBytes(): Uint8Array | undefined;
|
|
49
51
|
downloadOriginal(fileName?: string): Blob;
|
|
52
|
+
edit(options?: EditOptions): Promise<EditSession>;
|
|
53
|
+
getEditSession(): EditSession | undefined;
|
|
54
|
+
pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
|
|
55
|
+
clientToPage(clientX: number, clientY: number): PageHit | undefined;
|
|
50
56
|
on<K extends keyof ViewerEventMap>(type: K, listener: ViewerEventListener<K>): () => void;
|
|
51
57
|
destroy(): Promise<void>;
|
|
52
58
|
}
|
package/dist/viewer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EditSessionController, } from "./edit/session.js";
|
|
1
2
|
import { linkedAbortController } from "./abort.js";
|
|
2
3
|
import { detectFormat } from "./detect.js";
|
|
3
4
|
import { abortError, normalizeError, ViewerError } from "./errors.js";
|
|
@@ -28,6 +29,12 @@ export class DocumentViewer {
|
|
|
28
29
|
#info;
|
|
29
30
|
#original;
|
|
30
31
|
#originalFileName;
|
|
32
|
+
#originalContentType;
|
|
33
|
+
/** Limits the current document was opened with; reused when it is reopened. */
|
|
34
|
+
#limits;
|
|
35
|
+
#session;
|
|
36
|
+
/** Shared by concurrent `edit()` calls while the engine starts. */
|
|
37
|
+
#sessionStart;
|
|
31
38
|
#activeLoad;
|
|
32
39
|
#activeSearch;
|
|
33
40
|
#selection = null;
|
|
@@ -45,6 +52,7 @@ export class DocumentViewer {
|
|
|
45
52
|
constructor(options, runtime) {
|
|
46
53
|
this.#options = options;
|
|
47
54
|
this.#runtime = runtime;
|
|
55
|
+
this.#limits = runtime.limits;
|
|
48
56
|
this.#state = freezeState({
|
|
49
57
|
status: "idle",
|
|
50
58
|
pageIndex: 0,
|
|
@@ -77,6 +85,7 @@ export class DocumentViewer {
|
|
|
77
85
|
onTextSelection: (range) => void this.selectText(range),
|
|
78
86
|
onCellSelection: (selection) => this.#applyInteractiveCellSelection(selection),
|
|
79
87
|
getSelectionText: () => this.#getCachedSelectionText(),
|
|
88
|
+
onPainted: (pages, revision) => this.#onPainted(pages, revision),
|
|
80
89
|
}, options);
|
|
81
90
|
}
|
|
82
91
|
}
|
|
@@ -151,31 +160,21 @@ export class DocumentViewer {
|
|
|
151
160
|
await adapter.close(handle);
|
|
152
161
|
throw abortError();
|
|
153
162
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
backendInfo.
|
|
163
|
+
let info;
|
|
164
|
+
try {
|
|
165
|
+
info = describeDocument(adapter, backendInfo, detection.format, limits);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
157
168
|
await adapter.close(handle);
|
|
158
|
-
throw
|
|
159
|
-
details: {
|
|
160
|
-
pageCount: backendInfo.pageCount,
|
|
161
|
-
limit: limits.maxDocumentUnits,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
169
|
+
throw error;
|
|
164
170
|
}
|
|
165
|
-
const info = immutableInfo({
|
|
166
|
-
...backendInfo,
|
|
167
|
-
capabilities: {
|
|
168
|
-
textSelection: Boolean(adapter.getTextMap),
|
|
169
|
-
cellSelection: backendInfo.unit === "sheet",
|
|
170
|
-
search: Boolean(adapter.getTextMap),
|
|
171
|
-
thumbnails: backendInfo.unit !== "sheet",
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
171
|
this.#adapter = adapter;
|
|
175
172
|
this.#handle = handle;
|
|
176
173
|
this.#info = info;
|
|
177
174
|
this.#original = loaded.bytes;
|
|
178
175
|
this.#originalFileName = fileName;
|
|
176
|
+
this.#originalContentType = contentType;
|
|
177
|
+
this.#limits = limits;
|
|
179
178
|
this.#update({
|
|
180
179
|
status: "ready",
|
|
181
180
|
format: detection.format,
|
|
@@ -671,6 +670,45 @@ export class DocumentViewer {
|
|
|
671
670
|
}
|
|
672
671
|
return blob;
|
|
673
672
|
}
|
|
673
|
+
async edit(options = {}) {
|
|
674
|
+
const { adapter, info } = this.#assertReady();
|
|
675
|
+
if (this.#session?.core.usable)
|
|
676
|
+
return this.#session.session;
|
|
677
|
+
if (this.#sessionStart)
|
|
678
|
+
return this.#sessionStart;
|
|
679
|
+
const format = editableFormat(adapter, info.format);
|
|
680
|
+
if (!format || !adapter.edit)
|
|
681
|
+
throw new ViewerError("edit-unsupported", "Editing is not available for this document", { details: { format: info.format, reason: "no-engine" } });
|
|
682
|
+
const start = this.#startSession(adapter.edit, format, options);
|
|
683
|
+
this.#sessionStart = start;
|
|
684
|
+
try {
|
|
685
|
+
return await start;
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
if (this.#sessionStart === start)
|
|
689
|
+
this.#sessionStart = undefined;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
getEditSession() {
|
|
693
|
+
this.#assertAlive();
|
|
694
|
+
return this.#session?.session;
|
|
695
|
+
}
|
|
696
|
+
pageToClient(pageIndex, rect) {
|
|
697
|
+
this.#assertAlive();
|
|
698
|
+
if (this.#state.status !== "ready" ||
|
|
699
|
+
!Number.isInteger(pageIndex) ||
|
|
700
|
+
![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite))
|
|
701
|
+
return undefined;
|
|
702
|
+
return this.#viewport?.pageToClient(pageIndex, rect);
|
|
703
|
+
}
|
|
704
|
+
clientToPage(clientX, clientY) {
|
|
705
|
+
this.#assertAlive();
|
|
706
|
+
if (this.#state.status !== "ready" ||
|
|
707
|
+
!Number.isFinite(clientX) ||
|
|
708
|
+
!Number.isFinite(clientY))
|
|
709
|
+
return undefined;
|
|
710
|
+
return this.#viewport?.clientToPage(clientX, clientY);
|
|
711
|
+
}
|
|
674
712
|
on(type, listener) {
|
|
675
713
|
this.#assertAlive();
|
|
676
714
|
const listeners = this.#listeners.get(type) ?? new Set();
|
|
@@ -700,6 +738,194 @@ export class DocumentViewer {
|
|
|
700
738
|
this.#listeners.clear();
|
|
701
739
|
this.#runtime.release(this);
|
|
702
740
|
}
|
|
741
|
+
async #startSession(provider, format, options) {
|
|
742
|
+
const generation = this.#generation;
|
|
743
|
+
const operation = this.#startOperation(options.signal);
|
|
744
|
+
// A session that broke (failed recovery) makes way for a fresh engine.
|
|
745
|
+
const stale = this.#session;
|
|
746
|
+
this.#session = undefined;
|
|
747
|
+
if (stale)
|
|
748
|
+
await stale.core.end();
|
|
749
|
+
try {
|
|
750
|
+
const engine = await provider.load(this.#original.slice(), {
|
|
751
|
+
format,
|
|
752
|
+
limits: this.#limits,
|
|
753
|
+
fonts: this.#runtime.fonts.registered,
|
|
754
|
+
signal: operation.signal,
|
|
755
|
+
...(this.#originalFileName ? { fileName: this.#originalFileName } : {}),
|
|
756
|
+
...(this.#runtime.assetBaseUrl
|
|
757
|
+
? { assetBaseUrl: this.#runtime.assetBaseUrl }
|
|
758
|
+
: {}),
|
|
759
|
+
});
|
|
760
|
+
if (generation !== this.#generation || operation.signal.aborted) {
|
|
761
|
+
await engine.dispose().catch(() => undefined);
|
|
762
|
+
throw abortError();
|
|
763
|
+
}
|
|
764
|
+
const host = {
|
|
765
|
+
format,
|
|
766
|
+
limits: this.#limits,
|
|
767
|
+
prepareDocument: (bytes, signal) => this.#prepareDocument(bytes, signal),
|
|
768
|
+
commitDocument: (prepared, changedPages) => this.#commitDocument(prepared, changedPages),
|
|
769
|
+
discardDocument: (prepared) => this.#discardDocument(prepared),
|
|
770
|
+
emit: (type, event) => this.#emit(type, event),
|
|
771
|
+
pageOf: (paragraphId) => {
|
|
772
|
+
let first;
|
|
773
|
+
for (const [pageIndex, runs] of this.#textMaps)
|
|
774
|
+
if ((first === undefined || pageIndex < first) &&
|
|
775
|
+
runs.some((run) => run.paragraphId === paragraphId))
|
|
776
|
+
first = pageIndex;
|
|
777
|
+
return first;
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
const core = new EditSessionController(engine, host, this.#original, this.#info.pageCount);
|
|
781
|
+
const session = provider.createSession(core, {
|
|
782
|
+
getTextRuns: (pageIndex, signal) => this.#getTextRuns(pageIndex, signal),
|
|
783
|
+
cachedPages: () => [...this.#textMaps.keys()],
|
|
784
|
+
});
|
|
785
|
+
this.#session = { core, session };
|
|
786
|
+
this.#emit("editstatechange", {
|
|
787
|
+
...core.state,
|
|
788
|
+
active: true,
|
|
789
|
+
format,
|
|
790
|
+
});
|
|
791
|
+
return session;
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
const normalized = normalizeError(error, "edit-failed");
|
|
795
|
+
throw normalized.code === "edit-failed" && !normalized.details
|
|
796
|
+
? new ViewerError("edit-failed", normalized.message, {
|
|
797
|
+
cause: error,
|
|
798
|
+
details: { stage: "load" },
|
|
799
|
+
})
|
|
800
|
+
: normalized;
|
|
801
|
+
}
|
|
802
|
+
finally {
|
|
803
|
+
this.#finishOperation(operation);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Shows edited bytes as the current document: opens them through the
|
|
808
|
+
* adapter, swaps the handle, drops every cache derived from the old content
|
|
809
|
+
* and keeps the view where it was. Returns the new page count.
|
|
810
|
+
*/
|
|
811
|
+
/**
|
|
812
|
+
* Phase one of showing edited bytes: opens them next to the current
|
|
813
|
+
* document. Everything that can fail or be aborted happens here.
|
|
814
|
+
*/
|
|
815
|
+
async #prepareDocument(bytes, signal) {
|
|
816
|
+
const { adapter, handle, info } = this.#assertReady();
|
|
817
|
+
const generation = this.#generation;
|
|
818
|
+
enforceContainerLimits(bytes, info.format, this.#limits);
|
|
819
|
+
const context = {
|
|
820
|
+
format: info.format,
|
|
821
|
+
signal,
|
|
822
|
+
limits: this.#limits,
|
|
823
|
+
reportProgress: (progress) => this.#emit("progress", progress),
|
|
824
|
+
reportWarning: (warning) => this.#emit("warning", warning),
|
|
825
|
+
...(this.#originalFileName ? { fileName: this.#originalFileName } : {}),
|
|
826
|
+
...(this.#originalContentType
|
|
827
|
+
? { contentType: this.#originalContentType }
|
|
828
|
+
: {}),
|
|
829
|
+
...(this.#runtime.assetBaseUrl
|
|
830
|
+
? { assetBaseUrl: this.#runtime.assetBaseUrl }
|
|
831
|
+
: {}),
|
|
832
|
+
};
|
|
833
|
+
const next = adapter.reopen
|
|
834
|
+
? await adapter.reopen(handle, bytes, context)
|
|
835
|
+
: await adapter.open(bytes, context);
|
|
836
|
+
let nextInfo;
|
|
837
|
+
try {
|
|
838
|
+
if (generation !== this.#generation || signal.aborted)
|
|
839
|
+
throw abortError();
|
|
840
|
+
const backendInfo = await adapter.getInfo(next);
|
|
841
|
+
if (generation !== this.#generation || signal.aborted)
|
|
842
|
+
throw abortError();
|
|
843
|
+
nextInfo = describeDocument(adapter, backendInfo, info.format, this.#limits);
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
await adapter.close(next);
|
|
847
|
+
throw error;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
pageCount: nextInfo.pageCount,
|
|
851
|
+
handle: next,
|
|
852
|
+
info: nextInfo,
|
|
853
|
+
generation,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Phase two: swaps the prepared document in. Synchronous and never throws
|
|
858
|
+
* for a preparation of the current document, so a session that reached
|
|
859
|
+
* this point always completes; the old handle is closed in the background.
|
|
860
|
+
*/
|
|
861
|
+
#commitDocument(prepared, changedPages) {
|
|
862
|
+
if (prepared.generation !== this.#generation || !this.#handle) {
|
|
863
|
+
this.#discardDocument(prepared);
|
|
864
|
+
throw new ViewerError("lifecycle-error", "The document was replaced while an edit was being shown");
|
|
865
|
+
}
|
|
866
|
+
const { adapter, handle: previous } = this.#assertReady();
|
|
867
|
+
this.#handle = prepared.handle;
|
|
868
|
+
this.#info = prepared.info;
|
|
869
|
+
this.#invalidateContentCaches();
|
|
870
|
+
this.#update({
|
|
871
|
+
pageCount: prepared.info.pageCount,
|
|
872
|
+
pageIndex: Math.min(this.#state.pageIndex, prepared.info.pageCount - 1),
|
|
873
|
+
});
|
|
874
|
+
for (const warning of prepared.info.warnings ?? [])
|
|
875
|
+
this.#emit("warning", warning);
|
|
876
|
+
const revision = (this.#session?.core.state.revision ?? 0) + 1;
|
|
877
|
+
if (this.#viewport)
|
|
878
|
+
this.#viewport.replaceDocument(prepared.info, changedPages, revision);
|
|
879
|
+
else
|
|
880
|
+
// Headless: nothing paints, so geometry is "current" as soon as the
|
|
881
|
+
// session has emitted documentchange, which happens before the next
|
|
882
|
+
// macrotask.
|
|
883
|
+
setTimeout(() => this.#onPainted([...changedPages], revision), 0);
|
|
884
|
+
void Promise.resolve(adapter.close(previous)).catch((error) => this.#reportError(error));
|
|
885
|
+
return prepared.info.pageCount;
|
|
886
|
+
}
|
|
887
|
+
#discardDocument(prepared) {
|
|
888
|
+
void Promise.resolve(this.#adapter?.close(prepared.handle)).catch((error) => this.#reportError(error));
|
|
889
|
+
}
|
|
890
|
+
/** Painted pages of a content revision become a `layoutchange` while a session is active. */
|
|
891
|
+
#onPainted(pages, revision) {
|
|
892
|
+
const session = this.#session;
|
|
893
|
+
// The viewport reports only paints of its current content, so a revision
|
|
894
|
+
// the session has not committed yet is the only thing to drop.
|
|
895
|
+
if (!session || revision > session.core.state.revision)
|
|
896
|
+
return;
|
|
897
|
+
this.#emit("layoutchange", {
|
|
898
|
+
sessionId: session.core.sessionId,
|
|
899
|
+
revision,
|
|
900
|
+
pages: Object.freeze([...pages]),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
#reportError(error) {
|
|
904
|
+
const report = globalThis
|
|
905
|
+
.reportError;
|
|
906
|
+
if (report)
|
|
907
|
+
report(error);
|
|
908
|
+
else
|
|
909
|
+
console.error(error);
|
|
910
|
+
}
|
|
911
|
+
/** Forgets everything derived from the document's content, not from its view. */
|
|
912
|
+
#invalidateContentCaches() {
|
|
913
|
+
this.#textMaps.clear();
|
|
914
|
+
this.#textMapBytes = 0;
|
|
915
|
+
// The worker keeps its index; a stale key makes the next search rebuild it.
|
|
916
|
+
this.#fuzzyIndexKey = undefined;
|
|
917
|
+
this.#activeSearch?.abort();
|
|
918
|
+
this.#activeSearch = undefined;
|
|
919
|
+
this.#searchGeneration += 1;
|
|
920
|
+
if (this.#searchResult) {
|
|
921
|
+
this.#searchResult = null;
|
|
922
|
+
this.#emit("searchchange", null);
|
|
923
|
+
}
|
|
924
|
+
if (this.#selection) {
|
|
925
|
+
this.#selection = null;
|
|
926
|
+
this.#emit("selectionchange", null);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
703
929
|
async #getTextRuns(pageIndex, signal) {
|
|
704
930
|
const { adapter, handle, info } = this.#assertReady();
|
|
705
931
|
assertPageIndex(pageIndex, info.pageCount);
|
|
@@ -779,6 +1005,11 @@ export class DocumentViewer {
|
|
|
779
1005
|
this.#update({ panX: x, panY: y });
|
|
780
1006
|
}
|
|
781
1007
|
async #closeDocument() {
|
|
1008
|
+
const session = this.#session;
|
|
1009
|
+
this.#session = undefined;
|
|
1010
|
+
this.#sessionStart = undefined;
|
|
1011
|
+
if (session)
|
|
1012
|
+
await session.core.end();
|
|
782
1013
|
const adapter = this.#adapter;
|
|
783
1014
|
const handle = this.#handle;
|
|
784
1015
|
this.#adapter = undefined;
|
|
@@ -786,6 +1017,7 @@ export class DocumentViewer {
|
|
|
786
1017
|
this.#info = undefined;
|
|
787
1018
|
this.#original = undefined;
|
|
788
1019
|
this.#originalFileName = undefined;
|
|
1020
|
+
this.#originalContentType = undefined;
|
|
789
1021
|
this.#activeSearch?.abort();
|
|
790
1022
|
this.#activeSearch = undefined;
|
|
791
1023
|
this.#searchResult = null;
|
|
@@ -856,9 +1088,51 @@ export class DocumentViewer {
|
|
|
856
1088
|
}
|
|
857
1089
|
#emit(type, event) {
|
|
858
1090
|
for (const listener of this.#listeners.get(type) ?? [])
|
|
859
|
-
|
|
1091
|
+
try {
|
|
1092
|
+
listener(event);
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
// A listener's bug is its own; it never fails the call that emitted.
|
|
1096
|
+
this.#reportError(error);
|
|
1097
|
+
}
|
|
860
1098
|
}
|
|
861
1099
|
}
|
|
1100
|
+
/** The edit format of `format` when `adapter` can edit it. */
|
|
1101
|
+
/** The session format of a document format the adapter's provider edits. */
|
|
1102
|
+
function editableFormat(adapter, format) {
|
|
1103
|
+
const formats = adapter.edit?.formats ?? [];
|
|
1104
|
+
return formats.includes(format) ? SESSION_FORMATS[format] : undefined;
|
|
1105
|
+
}
|
|
1106
|
+
const SESSION_FORMATS = {
|
|
1107
|
+
pdf: "pdf",
|
|
1108
|
+
pptx: "pptx",
|
|
1109
|
+
pptm: "pptx",
|
|
1110
|
+
ppsx: "pptx",
|
|
1111
|
+
docx: "docx",
|
|
1112
|
+
docm: "docx",
|
|
1113
|
+
};
|
|
1114
|
+
/** Validates backend info and freezes it with the viewer's capability flags. */
|
|
1115
|
+
function describeDocument(adapter, backendInfo, format, limits) {
|
|
1116
|
+
if (!Number.isInteger(backendInfo.pageCount) ||
|
|
1117
|
+
backendInfo.pageCount < 1 ||
|
|
1118
|
+
backendInfo.pageCount > limits.maxDocumentUnits)
|
|
1119
|
+
throw new ViewerError("resource-limit", "Document unit count exceeds the configured limit", {
|
|
1120
|
+
details: {
|
|
1121
|
+
pageCount: backendInfo.pageCount,
|
|
1122
|
+
limit: limits.maxDocumentUnits,
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
return immutableInfo({
|
|
1126
|
+
...backendInfo,
|
|
1127
|
+
capabilities: {
|
|
1128
|
+
textSelection: Boolean(adapter.getTextMap),
|
|
1129
|
+
cellSelection: backendInfo.unit === "sheet",
|
|
1130
|
+
search: Boolean(adapter.getTextMap),
|
|
1131
|
+
thumbnails: backendInfo.unit !== "sheet",
|
|
1132
|
+
editing: editableFormat(adapter, format) !== undefined,
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
862
1136
|
function clampZoom(value) {
|
|
863
1137
|
if (!Number.isFinite(value))
|
|
864
1138
|
return 1;
|
package/dist/viewport.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CellRange, CellSelection, DocumentInfo, HeadlessRenderOptions, SearchMatch, TextRun, TextSelectionRange, ViewerState, SpreadsheetViewportRange } from "./contracts.js";
|
|
2
|
+
import type { PageHit, PageRect, ViewportRect } from "./edit/types.js";
|
|
2
3
|
export interface ViewportHost {
|
|
3
4
|
readonly state: ViewerState;
|
|
4
5
|
renderPage(pageIndex: number, target: HTMLCanvasElement, options: HeadlessRenderOptions): Promise<void>;
|
|
@@ -12,9 +13,15 @@ export interface ViewportHost {
|
|
|
12
13
|
onTextSelection(range: TextSelectionRange): void;
|
|
13
14
|
onCellSelection(selection: CellRange | CellSelection | null): void;
|
|
14
15
|
getSelectionText(): string | undefined;
|
|
16
|
+
/** Pages whose paint for content revision `revision` just finished. */
|
|
17
|
+
onPainted(pages: readonly number[], revision: number): void;
|
|
15
18
|
}
|
|
16
19
|
interface ViewportStrategy {
|
|
17
20
|
setDocument(info: DocumentInfo | undefined): void;
|
|
21
|
+
/** New content of the same document: keeps zoom and scroll, re-renders. */
|
|
22
|
+
replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
|
|
23
|
+
pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
|
|
24
|
+
clientToPage(clientX: number, clientY: number): PageHit | undefined;
|
|
18
25
|
update(): void;
|
|
19
26
|
panBy(deltaX: number, deltaY: number): void;
|
|
20
27
|
goToPage(pageIndex: number): void;
|
|
@@ -30,6 +37,9 @@ export declare class AdaptiveViewport implements ViewportStrategy {
|
|
|
30
37
|
readonly layout?: "continuous" | "single";
|
|
31
38
|
});
|
|
32
39
|
setDocument(info: DocumentInfo | undefined): void;
|
|
40
|
+
replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
|
|
41
|
+
pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
|
|
42
|
+
clientToPage(clientX: number, clientY: number): PageHit | undefined;
|
|
33
43
|
update(): void;
|
|
34
44
|
panBy(deltaX: number, deltaY: number): void;
|
|
35
45
|
goToPage(pageIndex: number): void;
|
|
@@ -45,6 +55,9 @@ export declare class ViewerViewport {
|
|
|
45
55
|
readonly layout?: "continuous" | "single";
|
|
46
56
|
});
|
|
47
57
|
setDocument(info: DocumentInfo | undefined): void;
|
|
58
|
+
replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
|
|
59
|
+
pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
|
|
60
|
+
clientToPage(clientX: number, clientY: number): PageHit | undefined;
|
|
48
61
|
update(): void;
|
|
49
62
|
panBy(deltaX: number, deltaY: number): void;
|
|
50
63
|
goToPage(pageIndex: number): void;
|