web-doc 0.6.2 → 0.7.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.
Files changed (188) hide show
  1. package/THIRD_PARTY_NOTICES.md +26 -1
  2. package/dist/adapters/docx-images.d.ts +13 -4
  3. package/dist/adapters/docx-images.js +3 -0
  4. package/dist/adapters/docx-paragraphs.d.ts +65 -0
  5. package/dist/adapters/docx-paragraphs.js +165 -0
  6. package/dist/adapters/docx-prepass.d.ts +17 -0
  7. package/dist/adapters/docx-prepass.js +230 -0
  8. package/dist/adapters/office.d.ts +31 -5
  9. package/dist/adapters/office.js +60 -29
  10. package/dist/adapters/pdf.d.ts +9 -0
  11. package/dist/adapters/pdf.js +10 -0
  12. package/dist/assets/pdfium/pdfium.wasm +0 -0
  13. package/dist/contracts.d.ts +47 -2
  14. package/dist/edit/assets.d.ts +23 -0
  15. package/dist/edit/assets.js +75 -0
  16. package/dist/edit/docx/elements.d.ts +6 -0
  17. package/dist/edit/docx/elements.js +90 -0
  18. package/dist/edit/docx/engine.d.ts +51 -0
  19. package/dist/edit/docx/engine.js +473 -0
  20. package/dist/edit/docx/handlers.d.ts +3 -0
  21. package/dist/edit/docx/handlers.js +15 -0
  22. package/dist/edit/docx/ids.d.ts +45 -0
  23. package/dist/edit/docx/ids.js +101 -0
  24. package/dist/edit/docx/model.d.ts +92 -0
  25. package/dist/edit/docx/model.js +339 -0
  26. package/dist/edit/docx/operations.d.ts +37 -0
  27. package/dist/edit/docx/operations.js +3 -0
  28. package/dist/edit/docx/provider.d.ts +13 -0
  29. package/dist/edit/docx/provider.js +34 -0
  30. package/dist/edit/docx/schemas.d.ts +6 -0
  31. package/dist/edit/docx/schemas.js +192 -0
  32. package/dist/edit/docx/session.d.ts +37 -0
  33. package/dist/edit/docx/session.js +426 -0
  34. package/dist/edit/docx/structure-ops.d.ts +6 -0
  35. package/dist/edit/docx/structure-ops.js +494 -0
  36. package/dist/edit/docx/style.d.ts +45 -0
  37. package/dist/edit/docx/style.js +375 -0
  38. package/dist/edit/docx/table-ops.d.ts +4 -0
  39. package/dist/edit/docx/table-ops.js +219 -0
  40. package/dist/edit/docx/text-ops.d.ts +18 -0
  41. package/dist/edit/docx/text-ops.js +458 -0
  42. package/dist/edit/docx/text.d.ts +34 -0
  43. package/dist/edit/docx/text.js +263 -0
  44. package/dist/edit/docx/types.d.ts +191 -0
  45. package/dist/edit/docx/types.js +1 -0
  46. package/dist/edit/docx/write.d.ts +50 -0
  47. package/dist/edit/docx/write.js +352 -0
  48. package/dist/edit/engine.d.ts +117 -0
  49. package/dist/edit/engine.js +1 -0
  50. package/dist/edit/history.d.ts +53 -0
  51. package/dist/edit/history.js +101 -0
  52. package/dist/edit/ooxml/names.d.ts +20 -0
  53. package/dist/edit/ooxml/names.js +61 -0
  54. package/dist/edit/ooxml/opc.d.ts +50 -0
  55. package/dist/edit/ooxml/opc.js +150 -0
  56. package/dist/edit/ooxml/package.d.ts +82 -0
  57. package/dist/edit/ooxml/package.js +233 -0
  58. package/dist/edit/ooxml/patch.d.ts +51 -0
  59. package/dist/edit/ooxml/patch.js +250 -0
  60. package/dist/edit/ooxml/transaction.d.ts +39 -0
  61. package/dist/edit/ooxml/transaction.js +316 -0
  62. package/dist/edit/ooxml/worker.d.ts +8 -0
  63. package/dist/edit/ooxml/worker.js +12 -0
  64. package/dist/edit/ooxml/writer.d.ts +21 -0
  65. package/dist/edit/ooxml/writer.js +187 -0
  66. package/dist/edit/ooxml/xml.d.ts +74 -0
  67. package/dist/edit/ooxml/xml.js +451 -0
  68. package/dist/edit/ooxml/zip.d.ts +54 -0
  69. package/dist/edit/ooxml/zip.js +280 -0
  70. package/dist/edit/operations.d.ts +19 -0
  71. package/dist/edit/operations.js +137 -0
  72. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  73. package/dist/edit/pdf/engine/compact.js +442 -0
  74. package/dist/edit/pdf/engine/document.d.ts +95 -0
  75. package/dist/edit/pdf/engine/document.js +868 -0
  76. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  77. package/dist/edit/pdf/engine/elements.js +313 -0
  78. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  79. package/dist/edit/pdf/engine/existing-text.js +424 -0
  80. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  81. package/dist/edit/pdf/engine/fonts.js +466 -0
  82. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  83. package/dist/edit/pdf/engine/geometry.js +97 -0
  84. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  85. package/dist/edit/pdf/engine/handler.js +108 -0
  86. package/dist/edit/pdf/engine/images.d.ts +33 -0
  87. package/dist/edit/pdf/engine/images.js +188 -0
  88. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  89. package/dist/edit/pdf/engine/layout.js +177 -0
  90. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  91. package/dist/edit/pdf/engine/operations.js +3 -0
  92. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  93. package/dist/edit/pdf/engine/pages.js +98 -0
  94. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  95. package/dist/edit/pdf/engine/pdfium.js +249 -0
  96. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  97. package/dist/edit/pdf/engine/shapes.js +183 -0
  98. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  99. package/dist/edit/pdf/engine/tables.js +315 -0
  100. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  101. package/dist/edit/pdf/engine/text-box.js +317 -0
  102. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  103. package/dist/edit/pdf/engine/text-layout.js +67 -0
  104. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  105. package/dist/edit/pdf/engine/transform.js +137 -0
  106. package/dist/edit/pdf/provider.d.ts +35 -0
  107. package/dist/edit/pdf/provider.js +92 -0
  108. package/dist/edit/pdf/range-map.d.ts +14 -0
  109. package/dist/edit/pdf/range-map.js +107 -0
  110. package/dist/edit/pdf/schemas.d.ts +11 -0
  111. package/dist/edit/pdf/schemas.js +291 -0
  112. package/dist/edit/pdf/selection.d.ts +5 -0
  113. package/dist/edit/pdf/selection.js +152 -0
  114. package/dist/edit/pdf/session.d.ts +53 -0
  115. package/dist/edit/pdf/session.js +273 -0
  116. package/dist/edit/pdf/types.d.ts +336 -0
  117. package/dist/edit/pdf/types.js +1 -0
  118. package/dist/edit/pptx/elements.d.ts +74 -0
  119. package/dist/edit/pptx/elements.js +301 -0
  120. package/dist/edit/pptx/engine.d.ts +37 -0
  121. package/dist/edit/pptx/engine.js +466 -0
  122. package/dist/edit/pptx/geometry.d.ts +72 -0
  123. package/dist/edit/pptx/geometry.js +193 -0
  124. package/dist/edit/pptx/handler.d.ts +7 -0
  125. package/dist/edit/pptx/handler.js +91 -0
  126. package/dist/edit/pptx/handlers.d.ts +3 -0
  127. package/dist/edit/pptx/handlers.js +21 -0
  128. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  129. package/dist/edit/pptx/image-table-ops.js +233 -0
  130. package/dist/edit/pptx/model.d.ts +69 -0
  131. package/dist/edit/pptx/model.js +170 -0
  132. package/dist/edit/pptx/operations.d.ts +50 -0
  133. package/dist/edit/pptx/operations.js +11 -0
  134. package/dist/edit/pptx/provider.d.ts +17 -0
  135. package/dist/edit/pptx/provider.js +40 -0
  136. package/dist/edit/pptx/schemas.d.ts +6 -0
  137. package/dist/edit/pptx/schemas.js +220 -0
  138. package/dist/edit/pptx/session.d.ts +44 -0
  139. package/dist/edit/pptx/session.js +114 -0
  140. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  141. package/dist/edit/pptx/shape-ops.js +486 -0
  142. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  143. package/dist/edit/pptx/slide-ops.js +353 -0
  144. package/dist/edit/pptx/style.d.ts +31 -0
  145. package/dist/edit/pptx/style.js +181 -0
  146. package/dist/edit/pptx/text-ops.d.ts +13 -0
  147. package/dist/edit/pptx/text-ops.js +267 -0
  148. package/dist/edit/pptx/text-write.d.ts +67 -0
  149. package/dist/edit/pptx/text-write.js +293 -0
  150. package/dist/edit/pptx/text.d.ts +41 -0
  151. package/dist/edit/pptx/text.js +99 -0
  152. package/dist/edit/pptx/types.d.ts +232 -0
  153. package/dist/edit/pptx/types.js +1 -0
  154. package/dist/edit/schema.d.ts +11 -0
  155. package/dist/edit/schema.js +275 -0
  156. package/dist/edit/session.d.ts +66 -0
  157. package/dist/edit/session.js +701 -0
  158. package/dist/edit/sessions.d.ts +8 -0
  159. package/dist/edit/sessions.js +1 -0
  160. package/dist/edit/types.d.ts +276 -0
  161. package/dist/edit/types.js +1 -0
  162. package/dist/edit/worker-engine.d.ts +31 -0
  163. package/dist/edit/worker-engine.js +91 -0
  164. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  165. package/dist/fonts/manifest.json +6 -1
  166. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  167. package/dist/fonts.d.ts +2 -0
  168. package/dist/fonts.js +4 -0
  169. package/dist/headless.d.ts +4 -0
  170. package/dist/headless.js +4 -0
  171. package/dist/index.d.ts +5 -0
  172. package/dist/index.js +5 -0
  173. package/dist/limits.js +3 -0
  174. package/dist/ooxml-edit-worker.d.ts +1 -0
  175. package/dist/ooxml-edit-worker.js +8 -0
  176. package/dist/pdf-edit-worker.d.ts +1 -0
  177. package/dist/pdf-edit-worker.js +35 -0
  178. package/dist/spreadsheet-viewport.d.ts +5 -0
  179. package/dist/spreadsheet-viewport.js +11 -0
  180. package/dist/ui.js +7 -0
  181. package/dist/viewer.d.ts +6 -0
  182. package/dist/viewer.js +293 -19
  183. package/dist/viewport.d.ts +13 -0
  184. package/dist/viewport.js +126 -1
  185. package/dist/worker-protocol.d.ts +39 -2
  186. package/dist/workers/ooxml-edit-worker.js +8671 -0
  187. package/dist/workers/pdf-edit-worker.js +10844 -0
  188. package/package.json +3 -3
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
- if (!Number.isInteger(backendInfo.pageCount) ||
155
- backendInfo.pageCount < 1 ||
156
- backendInfo.pageCount > limits.maxDocumentUnits) {
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 new ViewerError("resource-limit", "Document unit count exceeds the configured limit", {
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
- listener(event);
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;
@@ -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;
package/dist/viewport.js CHANGED
@@ -30,6 +30,19 @@ export class AdaptiveViewport {
30
30
  }
31
31
  this.#strategy.setDocument(info);
32
32
  }
33
+ replaceDocument(info, changedPages, revision) {
34
+ const nextKind = info.unit === "sheet" ? "sheet" : "page";
35
+ if (nextKind !== this.#kind)
36
+ this.setDocument(info);
37
+ else
38
+ this.#strategy.replaceDocument(info, changedPages, revision);
39
+ }
40
+ pageToClient(pageIndex, rect) {
41
+ return this.#strategy.pageToClient(pageIndex, rect);
42
+ }
43
+ clientToPage(clientX, clientY) {
44
+ return this.#strategy.clientToPage(clientX, clientY);
45
+ }
33
46
  update() {
34
47
  this.#strategy.update();
35
48
  }
@@ -70,6 +83,13 @@ export class ViewerViewport {
70
83
  #onSelectionChange = () => this.#handleSelectionChange();
71
84
  #pointers = new Map();
72
85
  #info;
86
+ /** Per page: grows when that page's content changed, so only it re-renders. */
87
+ #pageRevisions = [];
88
+ /** The session revision the shown content belongs to; 0 before any edit. */
89
+ #contentRevision = 0;
90
+ /** Pages painted since the last `onPainted`, keyed by content revision. */
91
+ #painted = new Map();
92
+ #paintedFlush = 0;
73
93
  #frame = 0;
74
94
  #resizeObserver;
75
95
  #destroyed = false;
@@ -126,11 +146,90 @@ export class ViewerViewport {
126
146
  }
127
147
  setDocument(info) {
128
148
  this.#info = info;
149
+ this.#pageRevisions = [];
150
+ this.#contentRevision = 0;
151
+ this.#painted.clear();
129
152
  this.#appliedZoom = this.#host.state.zoom;
130
153
  this.#root.scrollTo({ left: 0, top: 0 });
131
154
  this.#clearSlots();
132
155
  this.schedule();
133
156
  }
157
+ replaceDocument(info, changedPages, revision) {
158
+ const previous = this.#info;
159
+ this.#info = info;
160
+ this.#contentRevision = revision ?? this.#contentRevision + 1;
161
+ // Only pages whose content or size changed get a new render key; the
162
+ // others keep their bitmaps. A changed page count shifts everything
163
+ // from the first changed page on.
164
+ const countChanged = previous?.pageCount !== info.pageCount;
165
+ const from = changedPages === undefined
166
+ ? 0
167
+ : countChanged
168
+ ? Math.min(...changedPages, info.pageCount)
169
+ : Number.POSITIVE_INFINITY;
170
+ const changed = new Set(changedPages ?? []);
171
+ for (let pageIndex = 0; pageIndex < info.pageCount; pageIndex += 1) {
172
+ const size = info.pageSizes?.[pageIndex];
173
+ const before = previous?.pageSizes?.[pageIndex];
174
+ const resized = size?.width !== before?.width || size?.height !== before?.height;
175
+ if (pageIndex >= from || changed.has(pageIndex) || resized)
176
+ this.#pageRevisions[pageIndex] =
177
+ (this.#pageRevisions[pageIndex] ?? 0) + 1;
178
+ }
179
+ // Slots stay mounted and repaint in place; the browser clamps the scroll
180
+ // position itself once the spacer takes the new document's height.
181
+ this.schedule();
182
+ }
183
+ pageToClient(pageIndex, rect) {
184
+ const page = this.#mountedPage(pageIndex);
185
+ if (!page)
186
+ return undefined;
187
+ return {
188
+ left: page.left + rect.x * page.scale,
189
+ top: page.top + rect.y * page.scale,
190
+ width: rect.width * page.scale,
191
+ height: rect.height * page.scale,
192
+ };
193
+ }
194
+ clientToPage(clientX, clientY) {
195
+ for (const pageIndex of this.#slots.keys()) {
196
+ const page = this.#mountedPage(pageIndex);
197
+ if (!page ||
198
+ clientX < page.left ||
199
+ clientX >= page.left + page.width ||
200
+ clientY < page.top ||
201
+ clientY >= page.top + page.height)
202
+ continue;
203
+ return {
204
+ pageIndex,
205
+ point: {
206
+ x: (clientX - page.left) / page.scale,
207
+ y: (clientY - page.top) / page.scale,
208
+ },
209
+ };
210
+ }
211
+ return undefined;
212
+ }
213
+ /**
214
+ * Client-space box of a mounted page and its page-space scale, read from
215
+ * the DOM so it matches what is painted rather than what is scheduled.
216
+ */
217
+ #mountedPage(pageIndex) {
218
+ const slot = this.#slots.get(pageIndex);
219
+ if (!slot || !this.#info)
220
+ return undefined;
221
+ const box = slot.root.getBoundingClientRect();
222
+ const natural = naturalPageSize(this.#info, pageIndex);
223
+ if (box.width <= 0 || box.height <= 0)
224
+ return undefined;
225
+ return {
226
+ left: box.left,
227
+ top: box.top,
228
+ width: box.width,
229
+ height: box.height,
230
+ scale: box.width / natural.width,
231
+ };
232
+ }
134
233
  update() {
135
234
  const zoom = this.#host.state.zoom;
136
235
  if (this.#info &&
@@ -198,6 +297,28 @@ export class ViewerViewport {
198
297
  const size = naturalPageSize(this.#info, this.#host.state.pageIndex);
199
298
  return Math.max(0.1, Math.min(8, (this.#root.clientWidth - 24) / size.width, (this.#root.clientHeight - 24) / size.height));
200
299
  }
300
+ /** Collects finished paints and reports them once per frame, newest content only. */
301
+ #markPainted(pageIndex, contentRevision) {
302
+ if (contentRevision !== this.#contentRevision)
303
+ return;
304
+ let pages = this.#painted.get(contentRevision);
305
+ if (!pages) {
306
+ pages = new Set();
307
+ this.#painted.clear();
308
+ this.#painted.set(contentRevision, pages);
309
+ }
310
+ pages.add(pageIndex);
311
+ if (this.#paintedFlush)
312
+ return;
313
+ this.#paintedFlush = requestAnimationFrame(() => {
314
+ this.#paintedFlush = 0;
315
+ const current = this.#painted.get(this.#contentRevision);
316
+ this.#painted.clear();
317
+ if (!current || current.size === 0 || this.#destroyed)
318
+ return;
319
+ this.#host.onPainted([...current].sort((a, b) => a - b), this.#contentRevision);
320
+ });
321
+ }
201
322
  schedule() {
202
323
  if (this.#destroyed || this.#frame)
203
324
  return;
@@ -212,6 +333,8 @@ export class ViewerViewport {
212
333
  this.#destroyed = true;
213
334
  if (this.#frame)
214
335
  cancelAnimationFrame(this.#frame);
336
+ if (this.#paintedFlush)
337
+ cancelAnimationFrame(this.#paintedFlush);
215
338
  if (this.#zoomSettleTimer)
216
339
  clearTimeout(this.#zoomSettleTimer);
217
340
  this.#resizeObserver?.disconnect();
@@ -266,7 +389,7 @@ export class ViewerViewport {
266
389
  .getSearchMatches(pageIndex)
267
390
  .map((match) => `${match.start}:${match.end}`)
268
391
  .join(",");
269
- const renderKey = `${state.zoom}:${window.devicePixelRatio || 1}:${highlights}`;
392
+ const renderKey = `${this.#pageRevisions[pageIndex] ?? 0}:${state.zoom}:${window.devicePixelRatio || 1}:${highlights}`;
270
393
  if (slot.renderKey === undefined ||
271
394
  (!this.#zoomGestureActive && slot.renderKey !== renderKey)) {
272
395
  slot.renderKey = renderKey;
@@ -320,6 +443,7 @@ export class ViewerViewport {
320
443
  slot.controller = controller;
321
444
  const generation = ++slot.generation;
322
445
  const zoom = this.#host.state.zoom;
446
+ const contentRevision = this.#contentRevision;
323
447
  try {
324
448
  let renderError;
325
449
  const rendering = this.#host
@@ -347,6 +471,7 @@ export class ViewerViewport {
347
471
  const cssHeight = slot.canvas.height / (window.devicePixelRatio || 1);
348
472
  slot.root.style.width = `${cssWidth}px`;
349
473
  slot.root.style.height = `${cssHeight}px`;
474
+ this.#markPainted(pageIndex, contentRevision);
350
475
  }
351
476
  catch (error) {
352
477
  if (!controller.signal.aborted) {
@@ -1,5 +1,9 @@
1
1
  import type { DocumentFormat, DocumentInfo, ResourceLimits, TextRun, ViewerErrorData, ViewerProgress, ViewerWarning } from "./contracts.js";
2
- export type WorkerOperation = "init" | "open" | "get-info" | "render" | "get-text-map" | "close" | "destroy";
2
+ import type { EditableFormat } from "./edit/types.js";
3
+ export type DocumentWorkerOperation = "init" | "open" | "get-info" | "render" | "get-text-map" | "close" | "destroy";
4
+ /** Operations of an edit engine worker; payloads mirror the engine interface. */
5
+ export type EditWorkerOperation = "edit-init" | "edit-open" | "edit-validate" | "edit-apply" | "edit-materialize" | "edit-restore" | "edit-put-asset" | "edit-elements" | "edit-element" | "edit-elements-at" | "edit-find-text" | "edit-text-layout" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-dispose";
6
+ export type WorkerOperation = DocumentWorkerOperation | EditWorkerOperation;
3
7
  export interface WorkerRequest {
4
8
  readonly kind: "request";
5
9
  readonly id: number;
@@ -44,5 +48,38 @@ export interface WorkerRenderPayload {
44
48
  readonly zoom: number;
45
49
  readonly devicePixelRatio: number;
46
50
  }
47
- export type WorkerOperationResult = DocumentInfo | readonly TextRun[] | ImageBitmap | ArrayBuffer | undefined;
51
+ export interface EditWorkerInitPayload {
52
+ /** Where the worker fetches the engine's WebAssembly from. */
53
+ readonly wasmUrl: string;
54
+ /** TrueType font for text the standard PDF fonts cannot encode. */
55
+ readonly fallbackFontUrl?: string;
56
+ }
57
+ /** A host-registered font as the edit worker receives it. */
58
+ export interface EditWorkerFont {
59
+ readonly family: string;
60
+ readonly weight: number;
61
+ readonly style: "normal" | "italic" | "oblique";
62
+ /** Font bytes, or an absolute URL the worker fetches on first use. */
63
+ readonly source: ArrayBuffer | string;
64
+ }
65
+ export interface EditWorkerOpenPayload {
66
+ readonly data: ArrayBuffer;
67
+ readonly limits: ResourceLimits;
68
+ /** The session format; a worker that serves several formats picks its engine by it. */
69
+ readonly format?: EditableFormat;
70
+ readonly fileName?: string;
71
+ readonly fonts?: readonly EditWorkerFont[];
72
+ }
73
+ /** A rendered page as the worker returns it; `data` is transferred, not copied. */
74
+ export interface EditWorkerBitmap {
75
+ readonly pageIndex: number;
76
+ readonly scale: number;
77
+ readonly width: number;
78
+ readonly height: number;
79
+ readonly data: ArrayBuffer;
80
+ }
81
+ export interface EditWorkerOpenResult {
82
+ readonly pageCount: number;
83
+ }
84
+ export type WorkerOperationResult = DocumentInfo | readonly TextRun[] | ImageBitmap | ArrayBuffer | EditWorkerOpenResult | undefined;
48
85
  export declare function transferablesFor(value: unknown): Transferable[];