web-doc 0.8.0 → 0.10.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 +14 -0
- package/dist/adapters/office.js +2 -1
- package/dist/contracts.d.ts +9 -0
- package/dist/edit/ai/tools.js +3 -1
- package/dist/edit/docx/elements.js +19 -1
- package/dist/edit/docx/types.d.ts +8 -0
- package/dist/edit/pdf/engine/cff.d.ts +28 -0
- package/dist/edit/pdf/engine/cff.js +416 -0
- package/dist/edit/pdf/engine/document.d.ts +3 -1
- package/dist/edit/pdf/engine/document.js +69 -4
- package/dist/edit/pdf/engine/elements.d.ts +13 -0
- package/dist/edit/pdf/engine/elements.js +12 -1
- package/dist/edit/pdf/engine/existing-text.js +6 -4
- package/dist/edit/pdf/engine/fonts.js +13 -10
- package/dist/edit/pdf/engine/glyph-names.d.ts +2 -0
- package/dist/edit/pdf/engine/glyph-names.js +163 -0
- package/dist/edit/pdf/engine/handler.js +2 -0
- package/dist/edit/pdf/engine/opentype.d.ts +14 -0
- package/dist/edit/pdf/engine/opentype.js +317 -0
- package/dist/edit/pdf/engine/pages.js +9 -1
- package/dist/edit/pdf/engine/text-font.d.ts +4 -0
- package/dist/edit/pdf/engine/text-font.js +81 -0
- package/dist/edit/pdf/provider.d.ts +3 -1
- package/dist/edit/pdf/provider.js +3 -0
- package/dist/edit/pdf/schemas.js +4 -1
- package/dist/edit/pdf/session.d.ts +2 -1
- package/dist/edit/pdf/session.js +3 -0
- package/dist/edit/pdf/types.d.ts +55 -3
- package/dist/edit/pptx/session.d.ts +7 -2
- package/dist/edit/pptx/session.js +41 -3
- package/dist/fonts/THIRD_PARTY_NOTICES.md +6 -3
- package/dist/fonts/manifest.json +4 -4
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/viewport.js +83 -20
- package/dist/worker-protocol.d.ts +1 -1
- package/dist/workers/ooxml-edit-worker.js +15 -1
- package/dist/workers/pdf-edit-worker.js +1176 -197
- package/package.json +1 -1
|
@@ -11,8 +11,11 @@ export class PptxSession {
|
|
|
11
11
|
format = "pptx";
|
|
12
12
|
#core;
|
|
13
13
|
#tools;
|
|
14
|
-
|
|
14
|
+
/** The shown deck's text runs; absent in a headless session, which hits frames only. */
|
|
15
|
+
#access;
|
|
16
|
+
constructor(core, access) {
|
|
15
17
|
this.#core = core;
|
|
18
|
+
this.#access = access;
|
|
16
19
|
}
|
|
17
20
|
get sessionId() {
|
|
18
21
|
return this.#core.sessionId;
|
|
@@ -53,8 +56,31 @@ export class PptxSession {
|
|
|
53
56
|
getElement(id, options) {
|
|
54
57
|
return this.#core.getElement(id, options);
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The elements under a point, top-most first: those whose frame holds it,
|
|
61
|
+
* and before them a shape whose text is painted there past its frame, as
|
|
62
|
+
* text wrapped below a short box is.
|
|
63
|
+
*/
|
|
64
|
+
async elementsAt(pageIndex, point, options) {
|
|
65
|
+
const framed = (await this.#core.elementsAt(pageIndex, point, options));
|
|
66
|
+
const access = this.#access;
|
|
67
|
+
if (!access)
|
|
68
|
+
return framed;
|
|
69
|
+
const origins = (await access.getTextRuns(pageIndex, options?.signal))
|
|
70
|
+
.filter((run) => run.shapeOrigin && runHolds(run, point))
|
|
71
|
+
.map((run) => run.shapeOrigin);
|
|
72
|
+
if (origins.length === 0)
|
|
73
|
+
return framed;
|
|
74
|
+
const all = (await this.getElements({ pageIndex }, options)).items;
|
|
75
|
+
const painted = all.filter((element) => element.text !== undefined &&
|
|
76
|
+
!framed.items.some((hit) => hit.id === element.id) &&
|
|
77
|
+
origins.some((origin) => originOf(element, origin)));
|
|
78
|
+
if (painted.length === 0)
|
|
79
|
+
return framed;
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
...framed,
|
|
82
|
+
items: Object.freeze([...painted, ...framed.items]),
|
|
83
|
+
});
|
|
58
84
|
}
|
|
59
85
|
findText(query, options) {
|
|
60
86
|
return this.#core.findText(query, options);
|
|
@@ -143,3 +169,15 @@ function pptxReads(engine) {
|
|
|
143
169
|
throw new ViewerError("edit-unsupported", "The engine does not provide the PPTX reads", { details: { format: "pptx", reason: "no-reads" } });
|
|
144
170
|
return candidate;
|
|
145
171
|
}
|
|
172
|
+
/** Whether a run's box holds a point. */
|
|
173
|
+
function runHolds(run, point) {
|
|
174
|
+
return (point.x >= run.x &&
|
|
175
|
+
point.x <= run.x + run.width &&
|
|
176
|
+
point.y >= run.y &&
|
|
177
|
+
point.y <= run.y + run.height);
|
|
178
|
+
}
|
|
179
|
+
/** Whether a shape's frame starts where a run says its shape does, to half a pixel. */
|
|
180
|
+
function originOf(element, origin) {
|
|
181
|
+
const frame = element.frame ?? element.bounds;
|
|
182
|
+
return (Math.abs(frame.x - origin.x) <= 0.5 && Math.abs(frame.y - origin.y) <= 0.5);
|
|
183
|
+
}
|
|
@@ -6,8 +6,11 @@ The WOFF2 files in this directory are modified/subset builds of Noto Sans and No
|
|
|
6
6
|
- Noto Sans CJK sources: `notofonts/noto-cjk` commit `f8d157532fbfaeda587e826d4cd5b21a49186f7c`.
|
|
7
7
|
- License: SIL Open Font License 1.1; see `OFL-1.1.txt`.
|
|
8
8
|
- Modified font family used by the viewer: `Zrimo Noto`.
|
|
9
|
-
- `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset plus Noto
|
|
10
|
+
Sans's General Punctuation, Superscripts and Subscripts, Currency Symbols,
|
|
11
|
+
Letterlike Symbols and Number Forms (ranges in `manifest.json`; the Arrows
|
|
12
|
+
and Mathematical Operators ranges are requested too, but Noto Sans has no
|
|
13
|
+
glyphs there), saved as an uncompressed TrueType file; the PDF editor embeds it into
|
|
14
|
+
documents whose text the standard fonts cannot encode.
|
|
12
15
|
|
|
13
16
|
No proprietary Microsoft font is included.
|
package/dist/fonts/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"family": "Zrimo Noto",
|
|
4
|
-
"generated": "2026-
|
|
5
|
-
"tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs",
|
|
4
|
+
"generated": "2026-10-02",
|
|
5
|
+
"tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs, from hinted/ttf/NotoSans/NotoSans-Regular.ttf with U+0000,U+000D,U+0020-007E,U+00A0-024F,U+0300-036F,U+0400-052F,U+1E00-1EFF plus General Punctuation, Superscripts and Subscripts, Currency Symbols, Letterlike Symbols, Number Forms, Arrows, Mathematical Operators, U+25CC and U+FFFD (U+2000-206F,U+2070-209F,U+20A0-20CF,U+2100-214F,U+2150-218F,U+2190-21FF,U+2200-22FF,U+25CC,U+FFFD) so text typed with dashes, quotes, currency signs and the numero sign can be embedded; Noto Sans has no glyphs in the Arrows and Mathematical Operators ranges, so those two add none",
|
|
6
6
|
"license": "OFL-1.1",
|
|
7
7
|
"sources": {
|
|
8
8
|
"noto-fonts": "https://github.com/notofonts/noto-fonts/tree/ffebf8c1ee449e544955a7e813c54f9b73848eac",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"file": "noto-sans-latin-cyrillic.ttf",
|
|
84
|
-
"bytes":
|
|
85
|
-
"sha256": "
|
|
84
|
+
"bytes": 260552,
|
|
85
|
+
"sha256": "94094981da300fdb704d64cb91354360d37bf69b26f8584da7d44fdc2e0bdc84"
|
|
86
86
|
}
|
|
87
87
|
]
|
|
88
88
|
}
|
|
Binary file
|
package/dist/viewport.js
CHANGED
|
@@ -156,6 +156,8 @@ export class ViewerViewport {
|
|
|
156
156
|
}
|
|
157
157
|
replaceDocument(info, changedPages, revision) {
|
|
158
158
|
const previous = this.#info;
|
|
159
|
+
const painted = this.#painted.get(this.#contentRevision);
|
|
160
|
+
this.#painted.clear();
|
|
159
161
|
this.#info = info;
|
|
160
162
|
this.#contentRevision = revision ?? this.#contentRevision + 1;
|
|
161
163
|
// Only pages whose content or size changed get a new render key; the
|
|
@@ -172,9 +174,15 @@ export class ViewerViewport {
|
|
|
172
174
|
const size = info.pageSizes?.[pageIndex];
|
|
173
175
|
const before = previous?.pageSizes?.[pageIndex];
|
|
174
176
|
const resized = size?.width !== before?.width || size?.height !== before?.height;
|
|
175
|
-
if (pageIndex >= from || changed.has(pageIndex) || resized)
|
|
177
|
+
if (pageIndex >= from || changed.has(pageIndex) || resized) {
|
|
176
178
|
this.#pageRevisions[pageIndex] =
|
|
177
179
|
(this.#pageRevisions[pageIndex] ?? 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
else if (painted?.has(pageIndex) && this.#slots.has(pageIndex)) {
|
|
182
|
+
// A completed, retained page is still current. Carry its queued
|
|
183
|
+
// notification forward instead of losing it at the next frame.
|
|
184
|
+
this.#markPainted(pageIndex, this.#contentRevision);
|
|
185
|
+
}
|
|
178
186
|
}
|
|
179
187
|
// Slots stay mounted and repaint in place; the browser clamps the scroll
|
|
180
188
|
// position itself once the spacer takes the new document's height.
|
|
@@ -371,6 +379,8 @@ export class ViewerViewport {
|
|
|
371
379
|
slot.controller?.abort();
|
|
372
380
|
slot.root.remove();
|
|
373
381
|
this.#slots.delete(pageIndex);
|
|
382
|
+
for (const pages of this.#painted.values())
|
|
383
|
+
pages.delete(pageIndex);
|
|
374
384
|
}
|
|
375
385
|
for (let pageIndex = range.start; pageIndex < range.end; pageIndex += 1) {
|
|
376
386
|
const slot = this.#slots.get(pageIndex) ?? this.#createSlot(pageIndex);
|
|
@@ -443,41 +453,86 @@ export class ViewerViewport {
|
|
|
443
453
|
slot.controller = controller;
|
|
444
454
|
const generation = ++slot.generation;
|
|
445
455
|
const zoom = this.#host.state.zoom;
|
|
456
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
457
|
+
const pageRevision = this.#pageRevisions[pageIndex] ?? 0;
|
|
446
458
|
const contentRevision = this.#contentRevision;
|
|
459
|
+
// Adapters resize and paint their target asynchronously. Never give them
|
|
460
|
+
// the visible canvas: resizing clears it before the next frame is ready.
|
|
461
|
+
const canvas = document.createElement("canvas");
|
|
462
|
+
const textLayer = document.createElement("div");
|
|
463
|
+
const highlightLayer = document.createElement("div");
|
|
464
|
+
textLayer.style.cssText = slot.textLayer.style.cssText;
|
|
465
|
+
highlightLayer.style.cssText = slot.highlightLayer.style.cssText;
|
|
466
|
+
const isCurrent = () => !this.#destroyed &&
|
|
467
|
+
!controller.signal.aborted &&
|
|
468
|
+
this.#slots.get(pageIndex) === slot &&
|
|
469
|
+
slot.controller === controller &&
|
|
470
|
+
slot.generation === generation &&
|
|
471
|
+
(this.#pageRevisions[pageIndex] ?? 0) === pageRevision &&
|
|
472
|
+
this.#host.state.zoom === zoom &&
|
|
473
|
+
(window.devicePixelRatio || 1) === devicePixelRatio;
|
|
474
|
+
let rendering;
|
|
447
475
|
try {
|
|
448
|
-
|
|
449
|
-
const rendering = this.#host
|
|
450
|
-
.renderPage(pageIndex, slot.canvas, {
|
|
476
|
+
rendering = this.#host.renderPage(pageIndex, canvas, {
|
|
451
477
|
zoom,
|
|
452
|
-
devicePixelRatio
|
|
478
|
+
devicePixelRatio,
|
|
453
479
|
priority: pageIndex === this.#host.state.pageIndex ? "visible" : "adjacent",
|
|
454
480
|
signal: controller.signal,
|
|
481
|
+
});
|
|
482
|
+
const preparingText = this.#host
|
|
483
|
+
.getTextRuns(pageIndex, controller.signal)
|
|
484
|
+
.then(async (runs) => {
|
|
485
|
+
await rendering;
|
|
486
|
+
if (!isCurrent())
|
|
487
|
+
return;
|
|
488
|
+
await this.#buildTextLayers(textLayer, highlightLayer, pageIndex, runs, zoom, controller.signal);
|
|
455
489
|
})
|
|
456
|
-
.
|
|
457
|
-
|
|
490
|
+
.then(() => true, (error) => {
|
|
491
|
+
if (isCurrent())
|
|
492
|
+
slot.root.dataset.renderError =
|
|
493
|
+
error instanceof Error ? error.message : String(error);
|
|
494
|
+
// A valid raster still publishes without unavailable geometry.
|
|
495
|
+
// Never retain partial or previous text/highlight hit targets.
|
|
496
|
+
for (const layer of [textLayer, highlightLayer]) {
|
|
497
|
+
layer.replaceChildren();
|
|
498
|
+
resetOverlay(layer);
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
458
501
|
});
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
return;
|
|
462
|
-
await this.#buildTextLayers(slot.textLayer, slot.highlightLayer, pageIndex, runs, zoom, controller.signal);
|
|
463
|
-
if (controller.signal.aborted || slot.generation !== generation)
|
|
464
|
-
return;
|
|
465
|
-
await rendering;
|
|
466
|
-
if (renderError)
|
|
467
|
-
throw renderError;
|
|
468
|
-
if (controller.signal.aborted || slot.generation !== generation)
|
|
502
|
+
const [, textReady] = await Promise.all([rendering, preparingText]);
|
|
503
|
+
if (!isCurrent())
|
|
469
504
|
return;
|
|
470
|
-
const
|
|
471
|
-
|
|
505
|
+
const context = slot.canvas.getContext("2d");
|
|
506
|
+
if (!context)
|
|
507
|
+
throw new Error("Canvas 2D context is unavailable");
|
|
508
|
+
// Publish the completed raster and its matching text geometry in one
|
|
509
|
+
// synchronous commit, keeping the mounted page and layer identities.
|
|
510
|
+
slot.canvas.width = canvas.width;
|
|
511
|
+
slot.canvas.height = canvas.height;
|
|
512
|
+
context.drawImage(canvas, 0, 0);
|
|
513
|
+
replaceTextLayer(slot.textLayer, textLayer);
|
|
514
|
+
replaceTextLayer(slot.highlightLayer, highlightLayer);
|
|
515
|
+
const cssWidth = canvas.width / devicePixelRatio;
|
|
516
|
+
const cssHeight = canvas.height / devicePixelRatio;
|
|
472
517
|
slot.root.style.width = `${cssWidth}px`;
|
|
473
518
|
slot.root.style.height = `${cssHeight}px`;
|
|
519
|
+
if (textReady)
|
|
520
|
+
delete slot.root.dataset.renderError;
|
|
474
521
|
this.#markPainted(pageIndex, contentRevision);
|
|
475
522
|
}
|
|
476
523
|
catch (error) {
|
|
477
|
-
if (
|
|
524
|
+
if (isCurrent()) {
|
|
478
525
|
slot.root.dataset.renderError =
|
|
479
526
|
error instanceof Error ? error.message : String(error);
|
|
480
527
|
}
|
|
528
|
+
controller.abort();
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
// A cancelled adapter may still be writing. Release its backing store
|
|
532
|
+
// only once it has stopped, without affecting the committed page.
|
|
533
|
+
await rendering?.catch(() => undefined);
|
|
534
|
+
canvas.width = 0;
|
|
535
|
+
canvas.height = 0;
|
|
481
536
|
}
|
|
482
537
|
}
|
|
483
538
|
async #buildTextLayers(textLayer, highlightLayer, pageIndex, runs, zoom, signal) {
|
|
@@ -817,6 +872,7 @@ export class ViewerViewport {
|
|
|
817
872
|
});
|
|
818
873
|
}
|
|
819
874
|
#clearSlots() {
|
|
875
|
+
this.#painted.clear();
|
|
820
876
|
for (const slot of this.#slots.values()) {
|
|
821
877
|
slot.controller?.abort();
|
|
822
878
|
slot.root.remove();
|
|
@@ -824,6 +880,13 @@ export class ViewerViewport {
|
|
|
824
880
|
this.#slots.clear();
|
|
825
881
|
}
|
|
826
882
|
}
|
|
883
|
+
function replaceTextLayer(target, source) {
|
|
884
|
+
const children = document.createDocumentFragment();
|
|
885
|
+
while (source.firstChild)
|
|
886
|
+
children.append(source.firstChild);
|
|
887
|
+
target.style.cssText = source.style.cssText;
|
|
888
|
+
target.replaceChildren(children);
|
|
889
|
+
}
|
|
827
890
|
function naturalPageSize(info, pageIndex) {
|
|
828
891
|
const candidate = info?.pageSizes?.[pageIndex];
|
|
829
892
|
return candidate &&
|
|
@@ -2,7 +2,7 @@ import type { DocumentFormat, DocumentInfo, ResourceLimits, TextRun, ViewerError
|
|
|
2
2
|
import type { EditableFormat } from "./edit/types.js";
|
|
3
3
|
export type DocumentWorkerOperation = "init" | "open" | "get-info" | "render" | "get-text-map" | "close" | "destroy";
|
|
4
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-docx-revisions" | "edit-dispose";
|
|
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-text-font" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-docx-revisions" | "edit-dispose";
|
|
6
6
|
export type WorkerOperation = DocumentWorkerOperation | EditWorkerOperation;
|
|
7
7
|
export interface WorkerRequest {
|
|
8
8
|
readonly kind: "request";
|
|
@@ -3300,13 +3300,27 @@ function paragraphElement(model, record, base) {
|
|
|
3300
3300
|
id: record.elementId,
|
|
3301
3301
|
kind: "paragraph",
|
|
3302
3302
|
text: record.text.text,
|
|
3303
|
-
...record.table ? { parentId: record.table.elementId } : {},
|
|
3303
|
+
...record.table ? { parentId: record.table.elementId, ...cellOf(record.table, record) } : {},
|
|
3304
3304
|
textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
|
|
3305
3305
|
paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
|
|
3306
3306
|
...record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {},
|
|
3307
3307
|
operations: operationsOf("paragraph", record.readOnlyReason !== void 0)
|
|
3308
3308
|
};
|
|
3309
3309
|
}
|
|
3310
|
+
var CELLS = /* @__PURE__ */ new WeakMap();
|
|
3311
|
+
function cellOf(table, record) {
|
|
3312
|
+
let cells = CELLS.get(table);
|
|
3313
|
+
if (!cells) {
|
|
3314
|
+
cells = /* @__PURE__ */ new Map();
|
|
3315
|
+
for (const [row, cellsOfRow] of table.rows.entries())
|
|
3316
|
+
for (const [column, cell2] of cellsOfRow.entries())
|
|
3317
|
+
for (const paragraph of cell2.paragraphs)
|
|
3318
|
+
cells.set(paragraph, { row, column });
|
|
3319
|
+
CELLS.set(table, cells);
|
|
3320
|
+
}
|
|
3321
|
+
const cell = cells.get(record);
|
|
3322
|
+
return cell ? { cell } : {};
|
|
3323
|
+
}
|
|
3310
3324
|
|
|
3311
3325
|
// src/edit/docx/write.ts
|
|
3312
3326
|
var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|