web-doc 0.9.0 → 0.11.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 +6 -3
- package/dist/contracts.d.ts +9 -0
- package/dist/edit/docx/engine.d.ts +9 -1
- package/dist/edit/docx/engine.js +22 -0
- package/dist/edit/docx/provider.d.ts +3 -1
- package/dist/edit/docx/provider.js +3 -0
- package/dist/edit/docx/session.d.ts +6 -2
- package/dist/edit/docx/session.js +7 -1
- package/dist/edit/docx/style.d.ts +1 -1
- package/dist/edit/docx/style.js +3 -2
- package/dist/edit/docx/types.d.ts +11 -1
- 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 +21 -1
- package/dist/edit/pdf/engine/elements.d.ts +7 -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/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/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/session.d.ts +2 -1
- package/dist/edit/pdf/session.js +3 -0
- package/dist/edit/pdf/types.d.ts +40 -0
- package/dist/edit/pptx/elements.d.ts +4 -1
- package/dist/edit/pptx/elements.js +35 -0
- package/dist/edit/pptx/engine.d.ts +5 -1
- package/dist/edit/pptx/engine.js +11 -0
- package/dist/edit/pptx/handler.js +8 -0
- package/dist/edit/pptx/provider.d.ts +3 -1
- package/dist/edit/pptx/provider.js +3 -0
- package/dist/edit/pptx/session.d.ts +13 -4
- package/dist/edit/pptx/session.js +48 -4
- package/dist/edit/pptx/types.d.ts +11 -1
- package/dist/edit/range-style.d.ts +24 -0
- package/dist/edit/range-style.js +46 -0
- package/dist/viewport.js +83 -20
- package/dist/worker-protocol.d.ts +1 -1
- package/dist/workers/ooxml-edit-worker.js +90 -2
- package/dist/workers/pdf-edit-worker.js +1118 -184
- package/package.json +1 -1
package/dist/edit/pdf/types.d.ts
CHANGED
|
@@ -244,6 +244,41 @@ export interface TextLayoutLine {
|
|
|
244
244
|
readonly fontSize: number;
|
|
245
245
|
readonly color: string;
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* The font a PDF text element is drawn in, as a browser can load it: a host
|
|
249
|
+
* typing over the element shows the text in the document's own glyphs and
|
|
250
|
+
* metrics. Exactly one of `face` and `missing` is set.
|
|
251
|
+
*/
|
|
252
|
+
export interface TextFont {
|
|
253
|
+
readonly elementId: string;
|
|
254
|
+
/** The same for every element drawn in this font, so a host loads each face once. */
|
|
255
|
+
readonly key: string;
|
|
256
|
+
/** The family the file declares, as `textStyle.fontFamily` reports it. */
|
|
257
|
+
readonly family: string;
|
|
258
|
+
/**
|
|
259
|
+
* An OpenType or TrueType file for `FontFace`: a TrueType program with
|
|
260
|
+
* the OS/2, name and post tables a subset left out added, or a CFF program
|
|
261
|
+
* wrapped as OpenType with a Unicode cmap built from its glyph names (Adobe
|
|
262
|
+
* Glyph List names, `uniXXXX` and `uXXXX`). The program's own glyphs and
|
|
263
|
+
* widths are kept. Subset fonts hold only the glyphs the file uses, often
|
|
264
|
+
* not even a space, which the file places by positioning instead; a browser
|
|
265
|
+
* draws other characters in a fallback face. The file declares a
|
|
266
|
+
* regular face: its glyphs already carry the weight and slant, so load it
|
|
267
|
+
* with the default descriptors and do not ask the browser for bold or
|
|
268
|
+
* italic on top.
|
|
269
|
+
*/
|
|
270
|
+
readonly face?: {
|
|
271
|
+
readonly data: Uint8Array;
|
|
272
|
+
readonly format: "opentype" | "truetype";
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Why there is no face: the font is not embedded (a host shows it by name,
|
|
276
|
+
* as PDF readers do), or it is embedded in a form no browser loads: a
|
|
277
|
+
* CID-keyed CFF font, a Type 1 or Type 3 font, a font whose glyphs have no
|
|
278
|
+
* code points, or a program this reader cannot read.
|
|
279
|
+
*/
|
|
280
|
+
readonly missing?: "not-embedded" | "cid-keyed" | "type1" | "no-unicode" | "unreadable";
|
|
281
|
+
}
|
|
247
282
|
/** The drawn geometry of a `text`, `textBox` or `table` element. */
|
|
248
283
|
export interface TextLayout {
|
|
249
284
|
readonly elementId: string;
|
|
@@ -282,6 +317,11 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
|
|
|
282
317
|
save(options?: PdfSaveOptions): Promise<SavedDocument>;
|
|
283
318
|
/** Lines, glyph boxes and styles of a `text`, `textBox` or `table` element. */
|
|
284
319
|
getTextLayout(elementId: string, options?: ReadOptions): Promise<ReadItem<TextLayout>>;
|
|
320
|
+
/**
|
|
321
|
+
* The font a `text` or `textBox` element is drawn in, as a browser loads it,
|
|
322
|
+
* or why it cannot be loaded; see `TextFont`.
|
|
323
|
+
*/
|
|
324
|
+
getTextFont(elementId: string, options?: ReadOptions): Promise<ReadItem<TextFont>>;
|
|
285
325
|
/** The text position nearest to a page-space point; none on a page without text. */
|
|
286
326
|
positionAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadItem<TextPosition>>;
|
|
287
327
|
/** The rectangles a range covers, one per line fragment, in reading order. */
|
|
@@ -4,7 +4,8 @@ import { type Matrix, type PlacedFrame, type Xfrm } from "./geometry.js";
|
|
|
4
4
|
import type { DeckModel, SlideRecord } from "./model.js";
|
|
5
5
|
import { type StyleSource } from "./style.js";
|
|
6
6
|
import { type TextModel } from "./text.js";
|
|
7
|
-
import
|
|
7
|
+
import { type TextSpan } from "../range-style.js";
|
|
8
|
+
import type { PptxElement, PptxPlaceholder, PptxTextStyle } from "./types.js";
|
|
8
9
|
/** What the engine knows about one element beyond what it reports. */
|
|
9
10
|
export interface ShapeRecord {
|
|
10
11
|
readonly element: PptxElement;
|
|
@@ -28,6 +29,8 @@ export interface ShapeRecord {
|
|
|
28
29
|
/** Inside the fallback branch of `mc:AlternateContent`. */
|
|
29
30
|
readonly readOnly: boolean;
|
|
30
31
|
readonly parent?: ShapeRecord;
|
|
32
|
+
/** The style a span of the shape's text shows: what every run it covers shares. */
|
|
33
|
+
readonly styleOf?: (span: TextSpan) => Partial<PptxTextStyle> | undefined;
|
|
31
34
|
}
|
|
32
35
|
export interface SlideElements {
|
|
33
36
|
readonly slide: SlideRecord;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { groupMatrix, IDENTITY, multiply, placeFrame, readXfrm, } from "./geometry.js";
|
|
2
2
|
import { resolveTextStyle, readShapeStyle } from "./style.js";
|
|
3
3
|
import { firstTextRun, readTableCells, readTextModel, } from "./text.js";
|
|
4
|
+
import { runsCovering, sharedStyle } from "../range-style.js";
|
|
4
5
|
/*
|
|
5
6
|
* The elements of a slide, read from its XML: every child of the shape tree
|
|
6
7
|
* with its frame (own, or inherited from the layout and master for a
|
|
@@ -236,6 +237,11 @@ function readRecord(context, node, parents, parent, readOnly, walk) {
|
|
|
236
237
|
...(text ? { text } : {}),
|
|
237
238
|
readOnly,
|
|
238
239
|
...(parent ? { parent } : {}),
|
|
240
|
+
...(text && txBody
|
|
241
|
+
? {
|
|
242
|
+
styleOf: (span) => rangeStyleOf(context, placeholder, txBody, text, span),
|
|
243
|
+
}
|
|
244
|
+
: {}),
|
|
239
245
|
};
|
|
240
246
|
}
|
|
241
247
|
function operationsFor(kind, hasText) {
|
|
@@ -285,6 +291,35 @@ export function listStylesOf(context, txBody, placeholder) {
|
|
|
285
291
|
lists.push(defaults);
|
|
286
292
|
return lists;
|
|
287
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* The style a span of a text body shows: each run it covers resolves through
|
|
296
|
+
* its paragraph and the inherited lists, and the properties they all share
|
|
297
|
+
* are kept. An empty paragraph reads its end-of-paragraph properties.
|
|
298
|
+
*/
|
|
299
|
+
function rangeStyleOf(context, placeholder, txBody, text, span) {
|
|
300
|
+
const lists = listStylesOf(context, txBody, placeholder);
|
|
301
|
+
const styleOf = (paragraph, rPr) => resolveTextStyle({
|
|
302
|
+
...(rPr ? { rPr: { part: context.part, node: rPr } } : {}),
|
|
303
|
+
...(paragraph.pPr
|
|
304
|
+
? { pPr: { part: context.part, node: paragraph.pPr } }
|
|
305
|
+
: {}),
|
|
306
|
+
level: paragraph.level,
|
|
307
|
+
lists,
|
|
308
|
+
fonts: context.fonts,
|
|
309
|
+
});
|
|
310
|
+
const runs = text.paragraphs.flatMap((paragraph) => paragraph.runs.map((run) => ({
|
|
311
|
+
start: run.start,
|
|
312
|
+
end: run.end,
|
|
313
|
+
style: () => styleOf(paragraph, run.rPr),
|
|
314
|
+
})));
|
|
315
|
+
const covered = runsCovering(runs, span);
|
|
316
|
+
if (covered.length > 0)
|
|
317
|
+
return sharedStyle(covered.map((run) => run.style()));
|
|
318
|
+
const paragraph = text.paragraphs.find((candidate) => candidate.start <= span.start && span.start <= candidate.end) ?? text.paragraphs[0];
|
|
319
|
+
return paragraph
|
|
320
|
+
? sharedStyle([styleOf(paragraph, paragraph.endParaRPr)])
|
|
321
|
+
: undefined;
|
|
322
|
+
}
|
|
288
323
|
function textStyleOf(context, placeholder, txBody, text) {
|
|
289
324
|
const first = firstTextRun(text);
|
|
290
325
|
const paragraph = first?.paragraph ?? text.paragraphs[0];
|
|
@@ -3,11 +3,13 @@ import type { EditEngine, EngineBatch, EngineChange, MaterializedDocument, Mater
|
|
|
3
3
|
import { OoxmlPackage } from "../ooxml/package.js";
|
|
4
4
|
import type { EditFindOptions, EditOperation, ElementQuery, OperationIssue, PagePoint, TextTarget } from "../types.js";
|
|
5
5
|
import { DeckModel } from "./model.js";
|
|
6
|
-
import type { PptxElement, PptxLayoutInfo, PptxSlideInfo } from "./types.js";
|
|
6
|
+
import type { PptxElement, PptxLayoutInfo, PptxSlideInfo, PptxTextStyle } from "./types.js";
|
|
7
|
+
import { type TextSpan } from "../range-style.js";
|
|
7
8
|
/** The reads behind `getSlides` and `getLayouts`, beyond the core engine interface. */
|
|
8
9
|
export interface PptxEngineReads {
|
|
9
10
|
slides(signal: AbortSignal): Promise<readonly PptxSlideInfo[]>;
|
|
10
11
|
layouts(signal: AbortSignal): Promise<readonly PptxLayoutInfo[]>;
|
|
12
|
+
textStyle(id: string, span: TextSpan | undefined, signal: AbortSignal): Promise<Partial<PptxTextStyle> | undefined>;
|
|
11
13
|
}
|
|
12
14
|
export declare class PptxEditEngine implements EditEngine, PptxEngineReads {
|
|
13
15
|
#private;
|
|
@@ -32,6 +34,8 @@ export declare class PptxEditEngine implements EditEngine, PptxEngineReads {
|
|
|
32
34
|
elementsAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<readonly PptxElement[]>;
|
|
33
35
|
findText(query: string, options: EditFindOptions, signal: AbortSignal): Promise<readonly TextTarget[]>;
|
|
34
36
|
slides(signal: AbortSignal): Promise<readonly PptxSlideInfo[]>;
|
|
37
|
+
/** The style a span of a shape's text shows; none for an element without text or a span past it. */
|
|
38
|
+
textStyle(id: string, span: TextSpan | undefined, signal: AbortSignal): Promise<Partial<PptxTextStyle> | undefined>;
|
|
35
39
|
layouts(signal: AbortSignal): Promise<readonly PptxLayoutInfo[]>;
|
|
36
40
|
dispose(): Promise<void>;
|
|
37
41
|
}
|
package/dist/edit/pptx/engine.js
CHANGED
|
@@ -8,6 +8,7 @@ import { DeckModel } from "./model.js";
|
|
|
8
8
|
import { pptxHandlers } from "./handlers.js";
|
|
9
9
|
import { issueCollector, } from "./operations.js";
|
|
10
10
|
import { pptxOperationSchemas } from "./schemas.js";
|
|
11
|
+
import { spanFits } from "../range-style.js";
|
|
11
12
|
const SLIDE_PART = /^\/ppt\/slides\/slide(\d+)\.xml$/i;
|
|
12
13
|
const SLIDE_RELS = /^\/ppt\/slides\/_rels\/slide(\d+)\.xml\.rels$/i;
|
|
13
14
|
export class PptxEditEngine {
|
|
@@ -357,6 +358,16 @@ export class PptxEditEngine {
|
|
|
357
358
|
});
|
|
358
359
|
return out;
|
|
359
360
|
}
|
|
361
|
+
/** The style a span of a shape's text shows; none for an element without text or a span past it. */
|
|
362
|
+
async textStyle(id, span, signal) {
|
|
363
|
+
const record = await this.#locate(id, signal);
|
|
364
|
+
if (!record?.styleOf || !record.text)
|
|
365
|
+
return undefined;
|
|
366
|
+
const range = span ?? { start: 0, end: record.text.text.length };
|
|
367
|
+
if (!spanFits(range, record.text.text.length))
|
|
368
|
+
return undefined;
|
|
369
|
+
return record.styleOf(range);
|
|
370
|
+
}
|
|
360
371
|
async layouts(signal) {
|
|
361
372
|
const model = await this.model(signal);
|
|
362
373
|
return model.layouts.map((layout) => ({
|
|
@@ -88,6 +88,14 @@ export function createOoxmlEditHandler() {
|
|
|
88
88
|
return pptx().layouts(signal);
|
|
89
89
|
case "edit-docx-revisions":
|
|
90
90
|
return docx().revisions(payload.id, signal);
|
|
91
|
+
case "edit-docx-text-style": {
|
|
92
|
+
const { id, span } = payload;
|
|
93
|
+
return docx().textStyle(id, span, signal);
|
|
94
|
+
}
|
|
95
|
+
case "edit-pptx-text-style": {
|
|
96
|
+
const { id, span } = payload;
|
|
97
|
+
return pptx().textStyle(id, span, signal);
|
|
98
|
+
}
|
|
91
99
|
case "edit-dispose":
|
|
92
100
|
await state?.dispose();
|
|
93
101
|
state = undefined;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { EditEngineContext } from "../engine.js";
|
|
2
2
|
import { type OoxmlEditProviderOptions } from "../ooxml/worker.js";
|
|
3
3
|
import { WorkerEngineClient } from "../worker-engine.js";
|
|
4
|
+
import type { TextSpan } from "../range-style.js";
|
|
4
5
|
import type { PptxEngineReads } from "./engine.js";
|
|
5
|
-
import type { PptxLayoutInfo, PptxSlideInfo } from "./types.js";
|
|
6
|
+
import type { PptxLayoutInfo, PptxSlideInfo, PptxTextStyle } from "./types.js";
|
|
6
7
|
export type PptxEditProviderOptions = OoxmlEditProviderOptions;
|
|
7
8
|
/**
|
|
8
9
|
* Starts the OOXML edit worker for `original` as a PPTX engine. Loaded
|
|
@@ -14,4 +15,5 @@ export declare class PptxEditEngineClient extends WorkerEngineClient implements
|
|
|
14
15
|
start(original: Uint8Array): Promise<void>;
|
|
15
16
|
slides(signal: AbortSignal): Promise<readonly PptxSlideInfo[]>;
|
|
16
17
|
layouts(signal: AbortSignal): Promise<readonly PptxLayoutInfo[]>;
|
|
18
|
+
textStyle(id: string, span: TextSpan | undefined, signal: AbortSignal): Promise<Partial<PptxTextStyle> | undefined>;
|
|
17
19
|
}
|
|
@@ -37,4 +37,7 @@ export class PptxEditEngineClient extends WorkerEngineClient {
|
|
|
37
37
|
layouts(signal) {
|
|
38
38
|
return this.request("edit-pptx-layouts", undefined, signal);
|
|
39
39
|
}
|
|
40
|
+
textStyle(id, span, signal) {
|
|
41
|
+
return this.request("edit-pptx-text-style", { id, span }, signal);
|
|
42
|
+
}
|
|
40
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "../ai/types.js";
|
|
2
|
-
import type { EditSessionCore } from "../engine.js";
|
|
3
|
-
import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, SavedDocument, TextTarget } from "../types.js";
|
|
4
|
-
import type { PptxDeleteElementOperation, PptxDeleteSlideOperation, PptxDuplicateSlideOperation, PptxEditSession, PptxElement, PptxFields, PptxInsertImageOperation, PptxInsertSlideOperation, PptxInsertTableOperation, PptxInsertTextBoxOperation, PptxLayoutInfo, PptxMoveElementOperation, PptxMoveSlideOperation, PptxOperation, PptxReplaceTextOperation, PptxResizeElementOperation, PptxSaveOptions, PptxSetShapeStyleOperation, PptxSetTableCellOperation, PptxSetTextStyleOperation, PptxSlideInfo } from "./types.js";
|
|
2
|
+
import type { EditSessionAccess, EditSessionCore } from "../engine.js";
|
|
3
|
+
import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, SavedDocument, TextRange, TextTarget } from "../types.js";
|
|
4
|
+
import type { PptxDeleteElementOperation, PptxDeleteSlideOperation, PptxDuplicateSlideOperation, PptxEditSession, PptxElement, PptxFields, PptxInsertImageOperation, PptxInsertSlideOperation, PptxInsertTableOperation, PptxInsertTextBoxOperation, PptxLayoutInfo, PptxMoveElementOperation, PptxMoveSlideOperation, PptxOperation, PptxReplaceTextOperation, PptxResizeElementOperation, PptxSaveOptions, PptxSetShapeStyleOperation, PptxSetTableCellOperation, PptxSetTextStyleOperation, PptxSlideInfo, PptxTextStyle } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* The PPTX session: the core session narrowed to PPTX operations and
|
|
7
7
|
* elements, plus the slide and layout reads. Each typed method is `apply()`
|
|
@@ -10,7 +10,7 @@ import type { PptxDeleteElementOperation, PptxDeleteSlideOperation, PptxDuplicat
|
|
|
10
10
|
export declare class PptxSession implements PptxEditSession {
|
|
11
11
|
#private;
|
|
12
12
|
readonly format: "pptx";
|
|
13
|
-
constructor(core: EditSessionCore);
|
|
13
|
+
constructor(core: EditSessionCore, access?: EditSessionAccess);
|
|
14
14
|
get sessionId(): string;
|
|
15
15
|
get state(): EditState;
|
|
16
16
|
get schemas(): OperationSchemaSet;
|
|
@@ -24,6 +24,11 @@ export declare class PptxSession implements PptxEditSession {
|
|
|
24
24
|
addAsset(data: Uint8Array, options?: AssetOptions): Promise<string>;
|
|
25
25
|
getElements(query?: ElementQuery, options?: ReadOptions): Promise<ReadResult<PptxElement>>;
|
|
26
26
|
getElement(id: string, options?: ReadOptions): Promise<ReadItem<PptxElement>>;
|
|
27
|
+
/**
|
|
28
|
+
* The elements under a point, top-most first: those whose frame holds it,
|
|
29
|
+
* and before them a shape whose text is painted there past its frame, as
|
|
30
|
+
* text wrapped below a short box is.
|
|
31
|
+
*/
|
|
27
32
|
elementsAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadResult<PptxElement>>;
|
|
28
33
|
findText(query: string, options?: EditFindOptions): Promise<ReadResult<TextTarget>>;
|
|
29
34
|
getOutline(options?: OutlineOptions): Promise<OutlineResult>;
|
|
@@ -37,6 +42,10 @@ export declare class PptxSession implements PptxEditSession {
|
|
|
37
42
|
callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
|
|
38
43
|
getSlides(options?: ReadOptions): Promise<ReadResult<PptxSlideInfo>>;
|
|
39
44
|
getLayouts(options?: ReadOptions): Promise<ReadResult<PptxLayoutInfo>>;
|
|
45
|
+
getTextStyle(fields: {
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly range?: TextRange;
|
|
48
|
+
}, options?: ReadOptions): Promise<ReadItem<Partial<PptxTextStyle>>>;
|
|
40
49
|
replaceText(fields: PptxFields<PptxReplaceTextOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
41
50
|
setTextStyle(fields: PptxFields<PptxSetTextStyleOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
42
51
|
setShapeStyle(fields: PptxFields<PptxSetShapeStyleOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
@@ -2,6 +2,7 @@ import { ViewerError } from "../../errors.js";
|
|
|
2
2
|
import { readDescription, readOutline } from "../ai/outline.js";
|
|
3
3
|
import { resolveTargets } from "../ai/targets.js";
|
|
4
4
|
import { buildToolSet, callTool as runTool } from "../ai/tools.js";
|
|
5
|
+
import { spanOnTarget } from "../range-style.js";
|
|
5
6
|
/**
|
|
6
7
|
* The PPTX session: the core session narrowed to PPTX operations and
|
|
7
8
|
* elements, plus the slide and layout reads. Each typed method is `apply()`
|
|
@@ -11,8 +12,11 @@ export class PptxSession {
|
|
|
11
12
|
format = "pptx";
|
|
12
13
|
#core;
|
|
13
14
|
#tools;
|
|
14
|
-
|
|
15
|
+
/** The shown deck's text runs; absent in a headless session, which hits frames only. */
|
|
16
|
+
#access;
|
|
17
|
+
constructor(core, access) {
|
|
15
18
|
this.#core = core;
|
|
19
|
+
this.#access = access;
|
|
16
20
|
}
|
|
17
21
|
get sessionId() {
|
|
18
22
|
return this.#core.sessionId;
|
|
@@ -53,8 +57,31 @@ export class PptxSession {
|
|
|
53
57
|
getElement(id, options) {
|
|
54
58
|
return this.#core.getElement(id, options);
|
|
55
59
|
}
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The elements under a point, top-most first: those whose frame holds it,
|
|
62
|
+
* and before them a shape whose text is painted there past its frame, as
|
|
63
|
+
* text wrapped below a short box is.
|
|
64
|
+
*/
|
|
65
|
+
async elementsAt(pageIndex, point, options) {
|
|
66
|
+
const framed = (await this.#core.elementsAt(pageIndex, point, options));
|
|
67
|
+
const access = this.#access;
|
|
68
|
+
if (!access)
|
|
69
|
+
return framed;
|
|
70
|
+
const origins = (await access.getTextRuns(pageIndex, options?.signal))
|
|
71
|
+
.filter((run) => run.shapeOrigin && runHolds(run, point))
|
|
72
|
+
.map((run) => run.shapeOrigin);
|
|
73
|
+
if (origins.length === 0)
|
|
74
|
+
return framed;
|
|
75
|
+
const all = (await this.getElements({ pageIndex }, options)).items;
|
|
76
|
+
const painted = all.filter((element) => element.text !== undefined &&
|
|
77
|
+
!framed.items.some((hit) => hit.id === element.id) &&
|
|
78
|
+
origins.some((origin) => originOf(element, origin)));
|
|
79
|
+
if (painted.length === 0)
|
|
80
|
+
return framed;
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
...framed,
|
|
83
|
+
items: Object.freeze([...painted, ...framed.items]),
|
|
84
|
+
});
|
|
58
85
|
}
|
|
59
86
|
findText(query, options) {
|
|
60
87
|
return this.#core.findText(query, options);
|
|
@@ -92,6 +119,10 @@ export class PptxSession {
|
|
|
92
119
|
getLayouts(options) {
|
|
93
120
|
return this.#core.readItems(options, (engine, signal) => pptxReads(engine).layouts(signal));
|
|
94
121
|
}
|
|
122
|
+
async getTextStyle(fields, options) {
|
|
123
|
+
const span = spanOnTarget(fields.target, fields.range);
|
|
124
|
+
return this.#core.readItem(options, (engine, signal) => pptxReads(engine).textStyle(fields.target, span, signal));
|
|
125
|
+
}
|
|
95
126
|
replaceText(fields, options) {
|
|
96
127
|
return this.apply([{ op: "replaceText", ...fields }], options);
|
|
97
128
|
}
|
|
@@ -139,7 +170,20 @@ export class PptxSession {
|
|
|
139
170
|
function pptxReads(engine) {
|
|
140
171
|
const candidate = engine;
|
|
141
172
|
if (typeof candidate.slides !== "function" ||
|
|
142
|
-
typeof candidate.layouts !== "function"
|
|
173
|
+
typeof candidate.layouts !== "function" ||
|
|
174
|
+
typeof candidate.textStyle !== "function")
|
|
143
175
|
throw new ViewerError("edit-unsupported", "The engine does not provide the PPTX reads", { details: { format: "pptx", reason: "no-reads" } });
|
|
144
176
|
return candidate;
|
|
145
177
|
}
|
|
178
|
+
/** Whether a run's box holds a point. */
|
|
179
|
+
function runHolds(run, point) {
|
|
180
|
+
return (point.x >= run.x &&
|
|
181
|
+
point.x <= run.x + run.width &&
|
|
182
|
+
point.y >= run.y &&
|
|
183
|
+
point.y <= run.y + run.height);
|
|
184
|
+
}
|
|
185
|
+
/** Whether a shape's frame starts where a run says its shape does, to half a pixel. */
|
|
186
|
+
function originOf(element, origin) {
|
|
187
|
+
const frame = element.frame ?? element.bounds;
|
|
188
|
+
return (Math.abs(frame.x - origin.x) <= 0.5 && Math.abs(frame.y - origin.y) <= 0.5);
|
|
189
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApplyOptions, BinaryData, EditColor, EditElement, EditReceipt, EditSessionBase, PagePoint, PageRect, ReadOptions, ReadResult, SavedDocument, SaveOptions, TextRange } from "../types.js";
|
|
1
|
+
import type { ApplyOptions, BinaryData, EditColor, EditElement, EditReceipt, EditSessionBase, PagePoint, PageRect, ReadItem, ReadOptions, ReadResult, SavedDocument, SaveOptions, TextRange } from "../types.js";
|
|
2
2
|
export type PptxElementKind = "shape" | "image" | "table" | "connector" | "group" | "other";
|
|
3
3
|
export type PptxTextAlign = "left" | "center" | "right" | "justify";
|
|
4
4
|
/** The style of the first run with text, resolved through the placeholder chain and the theme. */
|
|
@@ -201,6 +201,16 @@ export interface PptxEditSession extends EditSessionBase<PptxOperation, PptxElem
|
|
|
201
201
|
getSlides(options?: ReadOptions): Promise<ReadResult<PptxSlideInfo>>;
|
|
202
202
|
/** Every layout of every master, for `insertSlide`. */
|
|
203
203
|
getLayouts(options?: ReadOptions): Promise<ReadResult<PptxLayoutInfo>>;
|
|
204
|
+
/**
|
|
205
|
+
* The text style a range of a shape's text shows: each property every run
|
|
206
|
+
* it covers shares, a property they differ on left out. Without a range,
|
|
207
|
+
* the whole text; a collapsed range reads the run before it, whose style
|
|
208
|
+
* text typed there takes. `undefined` for an element without text.
|
|
209
|
+
*/
|
|
210
|
+
getTextStyle(fields: {
|
|
211
|
+
readonly target: string;
|
|
212
|
+
readonly range?: TextRange;
|
|
213
|
+
}, options?: ReadOptions): Promise<ReadItem<Partial<PptxTextStyle>>>;
|
|
204
214
|
/** Replaces the whole text of a shape, or the part a range covers. */
|
|
205
215
|
replaceText(fields: PptxFields<PptxReplaceTextOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
206
216
|
/** Changes run and paragraph properties; unspecified ones keep their bytes. */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TextRange } from "./types.js";
|
|
2
|
+
/** Half-open UTF-16 offsets in an element's text. */
|
|
3
|
+
export interface TextSpan {
|
|
4
|
+
readonly start: number;
|
|
5
|
+
readonly end: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The span a range names on its target, or `undefined` for no range (the
|
|
9
|
+
* whole text). Both ends must lie on the target.
|
|
10
|
+
*/
|
|
11
|
+
export declare function spanOnTarget(target: string, range: TextRange | undefined): TextSpan | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The runs a span covers. A collapsed span reads the run before it, whose
|
|
14
|
+
* style text typed there takes, or the first run after it at the very start.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runsCovering<R extends TextSpan>(runs: readonly R[], span: TextSpan): R[];
|
|
17
|
+
/**
|
|
18
|
+
* What every style has in common: a property two of them differ on, or one
|
|
19
|
+
* some leave out, is left out. Values compare as JSON, so a theme colour and
|
|
20
|
+
* its plain form differ.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sharedStyle<T extends object>(styles: readonly T[]): Partial<T> | undefined;
|
|
23
|
+
/** Whether a span lies inside a text of `length` code units. */
|
|
24
|
+
export declare function spanFits(span: TextSpan, length: number): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ViewerError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The span a range names on its target, or `undefined` for no range (the
|
|
4
|
+
* whole text). Both ends must lie on the target.
|
|
5
|
+
*/
|
|
6
|
+
export function spanOnTarget(target, range) {
|
|
7
|
+
if (!range)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (range.start.elementId !== target || range.end.elementId !== target)
|
|
10
|
+
throw new ViewerError("invalid-operation", "Both ends of the range must lie on the target", { details: { target } });
|
|
11
|
+
return { start: range.start.offset, end: range.end.offset };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The runs a span covers. A collapsed span reads the run before it, whose
|
|
15
|
+
* style text typed there takes, or the first run after it at the very start.
|
|
16
|
+
*/
|
|
17
|
+
export function runsCovering(runs, span) {
|
|
18
|
+
if (span.start < span.end)
|
|
19
|
+
return runs.filter((run) => run.start < span.end && run.end > span.start);
|
|
20
|
+
const before = runs
|
|
21
|
+
.filter((run) => run.start < span.start && run.end >= span.start)
|
|
22
|
+
.at(-1);
|
|
23
|
+
const at = before ?? runs.find((run) => run.start >= span.start);
|
|
24
|
+
return at ? [at] : [];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What every style has in common: a property two of them differ on, or one
|
|
28
|
+
* some leave out, is left out. Values compare as JSON, so a theme colour and
|
|
29
|
+
* its plain form differ.
|
|
30
|
+
*/
|
|
31
|
+
export function sharedStyle(styles) {
|
|
32
|
+
const [first, ...rest] = styles;
|
|
33
|
+
if (!first)
|
|
34
|
+
return undefined;
|
|
35
|
+
const shared = {};
|
|
36
|
+
for (const key of Object.keys(first)) {
|
|
37
|
+
const value = JSON.stringify(first[key]);
|
|
38
|
+
if (rest.every((style) => key in style && JSON.stringify(style[key]) === value))
|
|
39
|
+
shared[key] = first[key];
|
|
40
|
+
}
|
|
41
|
+
return shared;
|
|
42
|
+
}
|
|
43
|
+
/** Whether a span lies inside a text of `length` code units. */
|
|
44
|
+
export function spanFits(span, length) {
|
|
45
|
+
return span.start >= 0 && span.start <= span.end && span.end <= length;
|
|
46
|
+
}
|
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-docx-text-style" | "edit-pptx-text-style" | "edit-dispose";
|
|
6
6
|
export type WorkerOperation = DocumentWorkerOperation | EditWorkerOperation;
|
|
7
7
|
export interface WorkerRequest {
|
|
8
8
|
readonly kind: "request";
|