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.
Files changed (38) hide show
  1. package/THIRD_PARTY_NOTICES.md +14 -0
  2. package/dist/adapters/office.js +2 -1
  3. package/dist/contracts.d.ts +9 -0
  4. package/dist/edit/ai/tools.js +3 -1
  5. package/dist/edit/docx/elements.js +19 -1
  6. package/dist/edit/docx/types.d.ts +8 -0
  7. package/dist/edit/pdf/engine/cff.d.ts +28 -0
  8. package/dist/edit/pdf/engine/cff.js +416 -0
  9. package/dist/edit/pdf/engine/document.d.ts +3 -1
  10. package/dist/edit/pdf/engine/document.js +69 -4
  11. package/dist/edit/pdf/engine/elements.d.ts +13 -0
  12. package/dist/edit/pdf/engine/elements.js +12 -1
  13. package/dist/edit/pdf/engine/existing-text.js +6 -4
  14. package/dist/edit/pdf/engine/fonts.js +13 -10
  15. package/dist/edit/pdf/engine/glyph-names.d.ts +2 -0
  16. package/dist/edit/pdf/engine/glyph-names.js +163 -0
  17. package/dist/edit/pdf/engine/handler.js +2 -0
  18. package/dist/edit/pdf/engine/opentype.d.ts +14 -0
  19. package/dist/edit/pdf/engine/opentype.js +317 -0
  20. package/dist/edit/pdf/engine/pages.js +9 -1
  21. package/dist/edit/pdf/engine/text-font.d.ts +4 -0
  22. package/dist/edit/pdf/engine/text-font.js +81 -0
  23. package/dist/edit/pdf/provider.d.ts +3 -1
  24. package/dist/edit/pdf/provider.js +3 -0
  25. package/dist/edit/pdf/schemas.js +4 -1
  26. package/dist/edit/pdf/session.d.ts +2 -1
  27. package/dist/edit/pdf/session.js +3 -0
  28. package/dist/edit/pdf/types.d.ts +55 -3
  29. package/dist/edit/pptx/session.d.ts +7 -2
  30. package/dist/edit/pptx/session.js +41 -3
  31. package/dist/fonts/THIRD_PARTY_NOTICES.md +6 -3
  32. package/dist/fonts/manifest.json +4 -4
  33. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  34. package/dist/viewport.js +83 -20
  35. package/dist/worker-protocol.d.ts +1 -1
  36. package/dist/workers/ooxml-edit-worker.js +15 -1
  37. package/dist/workers/pdf-edit-worker.js +1176 -197
  38. package/package.json +1 -1
@@ -0,0 +1,317 @@
1
+ import { cffWithSafeName } from "./cff.js";
2
+ /*
3
+ * Fonts a browser loads with `FontFace`. A PDF's CFF program gets a minimal
4
+ * OpenType font around it: the CFF table as the PDF holds it, a Unicode cmap,
5
+ * and the tables a font loader requires (head, hhea, hmtx, maxp, name, OS/2,
6
+ * post), filled from what the CFF says. A PDF's TrueType program keeps its
7
+ * tables; subsetting tools leave out the OS/2, name and post tables and pack
8
+ * tables off four-byte boundaries, which browsers refuse, so those tables are
9
+ * added and the file laid out again. Nothing in a program's tables changes.
10
+ */
11
+ const SFNT_TRUETYPE = 0x00010000;
12
+ const SFNT_CFF = 0x4f54544f; // "OTTO"
13
+ /**
14
+ * The OpenType file, or `undefined` when no glyph has a code point or the
15
+ * program's name cannot be made one a browser accepts.
16
+ */
17
+ export function cffOpenType(program, font, unicode) {
18
+ const safe = cffWithSafeName(program);
19
+ if (unicode.size === 0 || !safe)
20
+ return undefined;
21
+ const em = font.unitsPerEm;
22
+ const [xMin, yMin, xMax, yMax] = fontBox(font.bbox, em);
23
+ const glyphCount = font.widths.length;
24
+ const widths = font.widths.map((width) => Math.max(0, Math.min(0xffff, Math.round(width))));
25
+ const codes = [...unicode.keys()].sort((a, b) => a - b);
26
+ const tables = new Map([
27
+ ["CFF ", safe],
28
+ ["OS/2", os2(em, widths, codes, yMin, yMax)],
29
+ ["cmap", cmap(unicode, codes)],
30
+ ["head", head(em, xMin, yMin, xMax, yMax)],
31
+ ["hhea", hhea(widths, glyphCount, xMin, yMin, xMax, yMax)],
32
+ ["hmtx", concat(widths.map((width) => bytes(u16(width), i16(0))))],
33
+ ["maxp", bytes(u32(0x00005000), u16(glyphCount))],
34
+ ["name", names(postScriptName(font.name))],
35
+ ["post", post(em)],
36
+ ]);
37
+ return assemble(tables, SFNT_CFF);
38
+ }
39
+ /**
40
+ * The font box in int16 font units, in order; a font without one, which CFF
41
+ * allows, gets a box of the usual proportions so its line has a height.
42
+ */
43
+ function fontBox(box, em) {
44
+ const clamp = (value) => Math.max(-0x8000, Math.min(0x7fff, Math.round(value)));
45
+ const [x0 = 0, y0 = 0, x1 = 0, y1 = 0] = box.map(clamp);
46
+ if (y1 <= y0)
47
+ return [0, clamp(-em * 0.2), clamp(em), clamp(em * 0.8)];
48
+ return [Math.min(x0, x1), y0, Math.max(x0, x1), y1];
49
+ }
50
+ /**
51
+ * An embedded TrueType or OpenType program with the OS/2, name and post
52
+ * tables it lacks, laid out on four-byte boundaries; `undefined` when its
53
+ * table directory or core tables are broken.
54
+ */
55
+ export function completeSfnt(program, fontName) {
56
+ const tables = sfntTables(program);
57
+ const headTable = tables?.get("head");
58
+ const hheaTable = tables?.get("hhea");
59
+ const maxpTable = tables?.get("maxp");
60
+ const hmtxTable = tables?.get("hmtx");
61
+ if (!tables ||
62
+ !tables.has("cmap") ||
63
+ !headTable ||
64
+ headTable.length < 54 ||
65
+ !hheaTable ||
66
+ hheaTable.length < 36 ||
67
+ !maxpTable ||
68
+ maxpTable.length < 6 ||
69
+ !hmtxTable)
70
+ return undefined;
71
+ const view = (table) => new DataView(table.buffer, table.byteOffset, table.byteLength);
72
+ const headView = view(headTable);
73
+ const em = headView.getUint16(18);
74
+ const yMin = headView.getInt16(38);
75
+ const yMax = headView.getInt16(42);
76
+ // The adjustment is the file's, so it is worked out again.
77
+ const ownHead = headTable.slice();
78
+ ownHead.fill(0, 8, 12);
79
+ tables.set("head", ownHead);
80
+ if (!tables.has("OS/2")) {
81
+ const metrics = view(hheaTable).getUint16(34);
82
+ const widths = Array.from({ length: Math.min(metrics, hmtxTable.length >> 2) }, (_, glyph) => view(hmtxTable).getUint16(glyph * 4));
83
+ tables.set("OS/2", os2(em, widths, [], yMin, yMax));
84
+ }
85
+ if (!tables.has("name"))
86
+ tables.set("name", names(postScriptName(fontName)));
87
+ if (!tables.has("post"))
88
+ tables.set("post", post(em));
89
+ const version = view(program).getUint32(0);
90
+ return assemble(tables, version === SFNT_CFF ? SFNT_CFF : SFNT_TRUETYPE);
91
+ }
92
+ /** The sfnt file maps Unicode: a cmap subtable for platform 0, or for Windows Unicode BMP or full repertoire. */
93
+ export function hasUnicodeCmap(program) {
94
+ const table = sfntTables(program)?.get("cmap");
95
+ if (!table || table.length < 4)
96
+ return false;
97
+ const view = new DataView(table.buffer, table.byteOffset, table.byteLength);
98
+ const count = Math.min(view.getUint16(2), (table.length - 4) >> 3);
99
+ for (let index = 0; index < count; index += 1) {
100
+ const platform = view.getUint16(4 + index * 8);
101
+ const encoding = view.getUint16(6 + index * 8);
102
+ if (platform === 0 ||
103
+ (platform === 3 && (encoding === 1 || encoding === 10)))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ /** The tables of an sfnt file by tag, or `undefined` when its directory is broken. */
109
+ function sfntTables(program) {
110
+ if (program.length < 12)
111
+ return undefined;
112
+ const view = new DataView(program.buffer, program.byteOffset, program.byteLength);
113
+ const count = view.getUint16(4);
114
+ if (count === 0 || 12 + count * 16 > program.length)
115
+ return undefined;
116
+ const tables = new Map();
117
+ for (let index = 0; index < count; index += 1) {
118
+ const record = 12 + index * 16;
119
+ const tag = String.fromCharCode(...program.subarray(record, record + 4));
120
+ const offset = view.getUint32(record + 8);
121
+ const length = view.getUint32(record + 12);
122
+ if (tables.has(tag) || offset + length > program.length)
123
+ return undefined;
124
+ tables.set(tag, program.subarray(offset, offset + length));
125
+ }
126
+ return tables;
127
+ }
128
+ /** The PostScript name without a subset tag, in the characters a name table takes. */
129
+ function postScriptName(name) {
130
+ return (name
131
+ .replace(/^[A-Z]{6}\+/, "")
132
+ .replace(/[^\x21-\x7e]|[[\](){}<>/%]/g, "")
133
+ .slice(0, 63) || "Embedded");
134
+ }
135
+ function post(em) {
136
+ // Version 3: no glyph names.
137
+ return bytes(u32(0x00030000), u32(0), i16(-Math.round(em / 10)), i16(Math.round(em / 20)), u32(0), u32(0), u32(0), u32(0), u32(0));
138
+ }
139
+ function head(em, xMin, yMin, xMax, yMax) {
140
+ return bytes(u32(0x00010000), u32(0x00010000), u32(0), // checkSumAdjustment, set once the font is assembled
141
+ u32(0x5f0f3cf5), u16(0x000b), u16(em), u32(0), u32(0), u32(0), u32(0), i16(xMin), i16(yMin), i16(xMax), i16(yMax), u16(0), u16(8), i16(2), i16(0), i16(0));
142
+ }
143
+ function hhea(widths, glyphCount, xMin, yMin, xMax, yMax) {
144
+ return bytes(u32(0x00010000), i16(yMax), i16(yMin), i16(0), u16(widths.reduce((most, width) => Math.max(most, width), 0)), i16(xMin), i16(0), i16(xMax), i16(1), i16(0), i16(0), i16(0), i16(0), i16(0), i16(0), i16(0), u16(glyphCount));
145
+ }
146
+ function os2(em, widths, codes, yMin, yMax) {
147
+ const scaled = (fraction) => Math.round(em * fraction);
148
+ const drawn = widths.filter((width) => width > 0);
149
+ const average = drawn.length
150
+ ? Math.round(drawn.reduce((sum, width) => sum + width, 0) / drawn.length)
151
+ : 0;
152
+ return bytes(u16(4), i16(average), u16(400), u16(5), u16(0), i16(scaled(0.65)), i16(scaled(0.6)), i16(0), i16(scaled(0.075)), i16(scaled(0.65)), i16(scaled(0.6)), i16(0), i16(scaled(0.35)), i16(scaled(0.05)), i16(scaled(0.25)), i16(0), new Array(10).fill(0), u32(0), u32(0), u32(0), u32(0), [0x57, 0x44, 0x4f, 0x43], // "WDOC"
153
+ u16(0x0040), // regular
154
+ u16(Math.min(codes[0] ?? 0x20, 0xffff)), u16(Math.min(codes.at(-1) ?? 0xffff, 0xffff)), i16(yMax), i16(yMin), i16(0), u16(Math.max(0, yMax)), u16(Math.max(0, -yMin)), u32(1), u32(0), i16(scaled(0.5)), i16(scaled(0.7)), u16(0), u16(32), u16(0));
155
+ }
156
+ /**
157
+ * Format 4 for the BMP, and format 12 as well when a code point lies beyond
158
+ * it or the BMP takes more segments than format 4 holds. Records are in
159
+ * platform and encoding order, as the specification requires.
160
+ */
161
+ function cmap(unicode, codes) {
162
+ const basic = codes.filter((code) => code <= 0xffff);
163
+ const format4 = cmapFormat4(unicode, basic);
164
+ const format12 = !format4 || codes.length > basic.length
165
+ ? cmapFormat12(unicode, codes)
166
+ : undefined;
167
+ const subtables = [
168
+ ...(format4 ? [{ platform: 0, encoding: 3, data: format4 }] : []),
169
+ ...(format12 ? [{ platform: 0, encoding: 4, data: format12 }] : []),
170
+ ...(format4 ? [{ platform: 3, encoding: 1, data: format4 }] : []),
171
+ ...(format12 ? [{ platform: 3, encoding: 10, data: format12 }] : []),
172
+ ];
173
+ // Subtables shared by two records are written once.
174
+ const unique = [...new Set(subtables.map((subtable) => subtable.data))];
175
+ const headerSize = 4 + subtables.length * 8;
176
+ const offsets = new Map();
177
+ let offset = headerSize;
178
+ for (const data of unique) {
179
+ offsets.set(data, offset);
180
+ offset += data.length;
181
+ }
182
+ return concat([
183
+ bytes(u16(0), u16(subtables.length)),
184
+ ...subtables.map(({ platform, encoding, data }) => bytes(u16(platform), u16(encoding), u32(offsets.get(data)))),
185
+ ...unique,
186
+ ]);
187
+ }
188
+ /** `undefined` when the segments overflow the subtable's 16-bit length. */
189
+ function cmapFormat4(unicode, codes) {
190
+ // Runs of consecutive code points on consecutive glyphs share one delta.
191
+ const segments = [];
192
+ for (const code of codes) {
193
+ const glyph = unicode.get(code);
194
+ const last = segments.at(-1);
195
+ if (last &&
196
+ code === last.end + 1 &&
197
+ glyph === last.glyph + (code - last.start))
198
+ last.end = code;
199
+ else
200
+ segments.push({ start: code, end: code, glyph });
201
+ }
202
+ segments.push({ start: 0xffff, end: 0xffff, glyph: 0 });
203
+ const count = segments.length;
204
+ if (16 + count * 8 > 0xffff)
205
+ return undefined;
206
+ const searchRange = 2 * 2 ** Math.floor(Math.log2(count));
207
+ const delta = (segment) => segment.start === 0xffff ? 1 : (segment.glyph - segment.start) & 0xffff;
208
+ return bytes(u16(4), u16(16 + count * 8), u16(0), u16(count * 2), u16(searchRange), u16(Math.floor(Math.log2(count))), u16(count * 2 - searchRange), segments.flatMap((segment) => u16(segment.end)), u16(0), segments.flatMap((segment) => u16(segment.start)), segments.flatMap((segment) => u16(delta(segment))), segments.flatMap(() => u16(0)));
209
+ }
210
+ function cmapFormat12(unicode, codes) {
211
+ const groups = [];
212
+ for (const code of codes) {
213
+ const glyph = unicode.get(code);
214
+ const last = groups.at(-1);
215
+ if (last &&
216
+ code === last.end + 1 &&
217
+ glyph === last.glyph + (code - last.start))
218
+ last.end = code;
219
+ else
220
+ groups.push({ start: code, end: code, glyph });
221
+ }
222
+ return bytes(u16(12), u16(0), u32(16 + groups.length * 12), u32(0), u32(groups.length), groups.flatMap((group) => [
223
+ ...u32(group.start),
224
+ ...u32(group.end),
225
+ ...u32(group.glyph),
226
+ ]));
227
+ }
228
+ function names(postScript) {
229
+ const records = [
230
+ [1, postScript],
231
+ [2, "Regular"],
232
+ [3, `${postScript}-web-doc`],
233
+ [4, postScript],
234
+ [5, "Version 1.0"],
235
+ [6, postScript],
236
+ ];
237
+ const strings = records.map(([, text]) => [...text].flatMap((character) => u16(character.charCodeAt(0))));
238
+ let offset = 0;
239
+ const entries = records.map(([id], index) => {
240
+ const entry = [
241
+ ...u16(3),
242
+ ...u16(1),
243
+ ...u16(0x0409),
244
+ ...u16(id),
245
+ ...u16(strings[index].length),
246
+ ...u16(offset),
247
+ ];
248
+ offset += strings[index].length;
249
+ return entry;
250
+ });
251
+ return bytes(u16(0), u16(records.length), u16(6 + records.length * 12), entries.flat(), strings.flat());
252
+ }
253
+ /** The table directory, the tables padded to four bytes, and the head checksum adjustment. */
254
+ function assemble(tables, version) {
255
+ const tags = [...tables.keys()].sort();
256
+ const count = tags.length;
257
+ const selector = Math.floor(Math.log2(count));
258
+ const headerSize = 12 + count * 16;
259
+ let offset = headerSize;
260
+ const placed = tags.map((tag) => {
261
+ const data = tables.get(tag);
262
+ const at = offset;
263
+ offset += (data.length + 3) & ~3;
264
+ return { tag, data, at };
265
+ });
266
+ const font = new Uint8Array(offset);
267
+ font.set(bytes(u32(version), u16(count), u16(2 ** selector * 16), u16(selector), u16(count * 16 - 2 ** selector * 16), placed.flatMap(({ tag, data, at }) => [
268
+ ...[...tag].map((character) => character.charCodeAt(0)),
269
+ ...u32(checksum(data)),
270
+ ...u32(at),
271
+ ...u32(data.length),
272
+ ])));
273
+ for (const { data, at } of placed)
274
+ font.set(data, at);
275
+ const head = placed.find((table) => table.tag === "head");
276
+ const adjustment = (0xb1b0afba - checksum(font)) >>> 0;
277
+ font.set(u32(adjustment), head.at + 8);
278
+ return font;
279
+ }
280
+ function checksum(data) {
281
+ let sum = 0;
282
+ for (let offset = 0; offset < data.length; offset += 4)
283
+ sum =
284
+ (sum +
285
+ (((data[offset] ?? 0) << 24) |
286
+ ((data[offset + 1] ?? 0) << 16) |
287
+ ((data[offset + 2] ?? 0) << 8) |
288
+ (data[offset + 3] ?? 0))) >>>
289
+ 0;
290
+ return sum;
291
+ }
292
+ function u16(value) {
293
+ return [(value >> 8) & 0xff, value & 0xff];
294
+ }
295
+ function i16(value) {
296
+ return u16(value < 0 ? value + 0x10000 : value);
297
+ }
298
+ function u32(value) {
299
+ return [
300
+ (value >>> 24) & 0xff,
301
+ (value >>> 16) & 0xff,
302
+ (value >>> 8) & 0xff,
303
+ value & 0xff,
304
+ ];
305
+ }
306
+ function bytes(...parts) {
307
+ return concat(parts.map((part) => Uint8Array.from(part)));
308
+ }
309
+ function concat(parts) {
310
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
311
+ let offset = 0;
312
+ for (const part of parts) {
313
+ out.set(part, offset);
314
+ offset += part.length;
315
+ }
316
+ return out;
317
+ }
@@ -74,11 +74,19 @@ export const rotatePage = {
74
74
  validate(operation, context, issue) {
75
75
  if (operation.pageIndex >= context.pageCount)
76
76
  issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
77
+ // A field given as undefined counts as absent, as it does everywhere else.
78
+ if ((operation.rotation === undefined) === (operation.by === undefined))
79
+ issue("", "one-of", "Give exactly one of `rotation` and `by`");
77
80
  },
78
81
  apply(operation, context) {
79
82
  const { lib } = context.pdfium;
80
83
  context.withPage(operation.pageIndex, (page) => {
81
- lib.FPDFPage_SetRotation(page, operation.rotation / 90);
84
+ // A turn reads the angle the page has now, so it composes with what
85
+ // the file was saved with and with turns earlier in the history.
86
+ const quarters = operation.by !== undefined
87
+ ? (lib.FPDFPage_GetRotation(page) + operation.by / 90) % 4
88
+ : operation.rotation / 90;
89
+ lib.FPDFPage_SetRotation(page, quarters);
82
90
  });
83
91
  context.invalidatePage(operation.pageIndex);
84
92
  return {
@@ -0,0 +1,4 @@
1
+ import type { TextFont } from "../types.js";
2
+ import type { Pdfium } from "./pdfium.js";
3
+ export type TextFace = Omit<TextFont, "elementId">;
4
+ export declare function textFaceOf(pdfium: Pdfium, object: number, cache: Map<string, TextFace>): TextFace;
@@ -0,0 +1,81 @@
1
+ import { glyphUnicode, readCff } from "./cff.js";
2
+ import { textStyle } from "./elements.js";
3
+ import { cffOpenType, completeSfnt, hasUnicodeCmap } from "./opentype.js";
4
+ export function textFaceOf(pdfium, object, cache) {
5
+ const { lib } = pdfium;
6
+ const font = lib.FPDFTextObj_GetFont(object);
7
+ const family = textStyle(pdfium, object).fontFamily;
8
+ const baseName = pdfium.readUtf8String((buffer, bytes) => lib.FPDFFont_GetBaseFontName(font, buffer, bytes));
9
+ if (!lib.FPDFFont_GetIsEmbedded(font))
10
+ return { key: `name:${baseName}`, family, missing: "not-embedded" };
11
+ // A Type 3 font is embedded as drawings, with no program to read.
12
+ const program = fontProgram(pdfium, font);
13
+ if (!program)
14
+ return { key: `embedded:${baseName}`, family, missing: "unreadable" };
15
+ const key = `${baseName}#${fnv1a(program).toString(16)}`;
16
+ const known = cache.get(key);
17
+ if (known)
18
+ return { ...known, family };
19
+ const face = { key, family, ...faceOf(program, baseName) };
20
+ cache.set(key, face);
21
+ return face;
22
+ }
23
+ function faceOf(program, baseName) {
24
+ const tag = String.fromCharCode(...program.subarray(0, 4));
25
+ const truetype = tag === "true" ||
26
+ (program[0] === 0 &&
27
+ program[1] === 1 &&
28
+ program[2] === 0 &&
29
+ program[3] === 0);
30
+ if (truetype || tag === "OTTO") {
31
+ if (!hasUnicodeCmap(program))
32
+ return { missing: "no-unicode" };
33
+ const data = completeSfnt(program, baseName);
34
+ return data
35
+ ? { face: { data, format: truetype ? "truetype" : "opentype" } }
36
+ : { missing: "unreadable" };
37
+ }
38
+ // Type 1 programs start with "%!" in clear text, or with a PFB segment.
39
+ if (tag.startsWith("%!") || program[0] === 0x80)
40
+ return { missing: "type1" };
41
+ const cff = readCff(program);
42
+ if (!cff)
43
+ return { missing: "unreadable" };
44
+ if (cff.cid)
45
+ return { missing: "cid-keyed" };
46
+ const unicode = new Map();
47
+ cff.glyphNames.forEach((name, glyph) => {
48
+ const code = glyphUnicode(name);
49
+ if (code !== undefined && !unicode.has(code))
50
+ unicode.set(code, glyph);
51
+ });
52
+ const data = cffOpenType(program, cff, unicode);
53
+ return data
54
+ ? { face: { data, format: "opentype" } }
55
+ : { missing: "no-unicode" };
56
+ }
57
+ /** The font program a PDF font object embeds, or `undefined` for a font it does not embed. */
58
+ function fontProgram(pdfium, font) {
59
+ const { lib } = pdfium;
60
+ const size = pdfium.readNumbers(1, "i32", ([out]) => lib.FPDFFont_GetFontData(font, 0, 0, out))?.[0];
61
+ if (!size)
62
+ return undefined;
63
+ const buffer = pdfium.malloc(size);
64
+ const out = pdfium.malloc(4);
65
+ try {
66
+ return lib.FPDFFont_GetFontData(font, buffer, size, out)
67
+ ? pdfium.readBytes(buffer, size)
68
+ : undefined;
69
+ }
70
+ finally {
71
+ pdfium.free(out);
72
+ pdfium.free(buffer);
73
+ }
74
+ }
75
+ /** FNV-1a over the program: a cheap, stable key for one font. */
76
+ function fnv1a(bytes) {
77
+ let hash = 0x811c9dc5;
78
+ for (const byte of bytes)
79
+ hash = Math.imul(hash ^ byte, 0x01000193);
80
+ return hash >>> 0;
81
+ }
@@ -2,10 +2,11 @@ import { type WorkerLike } from "../../worker-client.js";
2
2
  import type { EditEngineContext } from "../engine.js";
3
3
  import type { PagePoint, PageRect, TextPosition, TextRange } from "../types.js";
4
4
  import { WorkerEngineClient } from "../worker-engine.js";
5
- import type { PageBitmap, PageLayout, TextLayout } from "./types.js";
5
+ import type { PageBitmap, PageLayout, TextFont, TextLayout } from "./types.js";
6
6
  /** The reads behind the overlay primitives, beyond the core engine interface. */
7
7
  export interface PdfEngineReads {
8
8
  textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
9
+ textFont(id: string, signal: AbortSignal): Promise<TextFont | undefined>;
9
10
  positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
10
11
  rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
11
12
  renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
@@ -28,6 +29,7 @@ export declare class PdfEditEngineClient extends WorkerEngineClient implements P
28
29
  readonly schemas: import("../types.js").OperationSchemaSet;
29
30
  start(original: Uint8Array, options: PdfEditProviderOptions): Promise<void>;
30
31
  textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
32
+ textFont(id: string, signal: AbortSignal): Promise<TextFont | undefined>;
31
33
  positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
32
34
  rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
33
35
  renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
@@ -53,6 +53,9 @@ export class PdfEditEngineClient extends WorkerEngineClient {
53
53
  textLayout(id, signal) {
54
54
  return this.request("edit-text-layout", { id }, signal);
55
55
  }
56
+ textFont(id, signal) {
57
+ return this.request("edit-text-font", { id }, signal);
58
+ }
56
59
  positionAt(pageIndex, point, signal) {
57
60
  return this.request("edit-position-at", { pageIndex, point }, signal);
58
61
  }
@@ -170,10 +170,13 @@ export const pdfOperationSchemas = Object.freeze({
170
170
  from: { type: "integer", minimum: 0 },
171
171
  to: { type: "integer", minimum: 0 },
172
172
  }, ["from", "to"]),
173
+ // Exactly one of `rotation` (absolute) and `by` (a turn from the current
174
+ // angle); the handler reports a batch that gives both or neither.
173
175
  rotatePage: operation("rotatePage", {
174
176
  pageIndex: { type: "integer", minimum: 0 },
175
177
  rotation: { enum: [0, 90, 180, 270] },
176
- }, ["pageIndex", "rotation"]),
178
+ by: { enum: [90, 180, 270] },
179
+ }, ["pageIndex"]),
177
180
  insertShape: operation("insertShape", {
178
181
  pageIndex: { type: "integer", minimum: 0 },
179
182
  shape: { enum: ["rectangle", "ellipse", "line"] },
@@ -2,7 +2,7 @@ import type { TextSelection } from "../../contracts.js";
2
2
  import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "../ai/types.js";
3
3
  import type { EditSessionCore } from "../engine.js";
4
4
  import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, PageRect, ReadItem, ReadOptions, ReadResult, SavedDocument, TextPosition, TextRange, TextTarget } from "../types.js";
5
- import type { DeleteElementOperation, DeletePageOperation, Fields, InsertImageOperation, InsertPageOperation, InsertShapeOperation, InsertTableOperation, InsertTextBoxOperation, MoveElementOperation, MovePageOperation, PdfEditSession, PdfElement, PdfOperation, PdfSaveOptions, ReplaceTextOperation, ResizeElementOperation, RotatePageOperation, SetShapeStyleOperation, SetTableCellOperation, PageBitmap, PageLayout, RenderOptions, SetTextStyleOperation, TextLayout } from "./types.js";
5
+ import type { DeleteElementOperation, DeletePageOperation, Fields, InsertImageOperation, InsertPageOperation, InsertShapeOperation, InsertTableOperation, InsertTextBoxOperation, MoveElementOperation, MovePageOperation, PdfEditSession, PdfElement, PdfOperation, PdfSaveOptions, ReplaceTextOperation, ResizeElementOperation, RotatePageOperation, SetShapeStyleOperation, SetTableCellOperation, PageBitmap, PageLayout, RenderOptions, SetTextStyleOperation, TextFont, TextLayout } from "./types.js";
6
6
  /**
7
7
  * The PDF session: the core session narrowed to PDF operations and elements.
8
8
  * Typed methods are added here as their operations ship; each is `apply()`
@@ -39,6 +39,7 @@ export declare class PdfSession implements PdfEditSession {
39
39
  get tools(): ToolSet;
40
40
  callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
41
41
  getTextLayout(elementId: string, options?: ReadOptions): Promise<ReadItem<TextLayout>>;
42
+ getTextFont(elementId: string, options?: ReadOptions): Promise<ReadItem<TextFont>>;
42
43
  positionAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadItem<TextPosition>>;
43
44
  rangeRects(range: TextRange, options?: ReadOptions): Promise<ReadResult<PageRect>>;
44
45
  getPageLayout(pageIndex: number, options?: ReadOptions): Promise<ReadItem<PageLayout>>;
@@ -198,6 +198,9 @@ export class PdfSession {
198
198
  getTextLayout(elementId, options) {
199
199
  return this.#core.readItem(options, (engine, signal) => pdfReads(engine).textLayout(elementId, signal));
200
200
  }
201
+ getTextFont(elementId, options) {
202
+ return this.#core.readItem(options, (engine, signal) => pdfReads(engine).textFont(elementId, signal));
203
+ }
201
204
  positionAt(pageIndex, point, options) {
202
205
  return this.#core.readItem(options, (engine, signal) => pdfReads(engine).positionAt(pageIndex, point, signal));
203
206
  }
@@ -192,12 +192,24 @@ export interface SetShapeStyleOperation {
192
192
  /** A new fill, `null` to remove it, or absent to keep it. */
193
193
  readonly fill?: PdfFill | null;
194
194
  }
195
- export interface RotatePageOperation {
195
+ /**
196
+ * Sets a page's rotation, or turns it from where it stands: a host offering
197
+ * "rotate" does not need to know the angle a file was saved with, and a
198
+ * replay of the history turns from the same angle again.
199
+ */
200
+ export type RotatePageOperation = {
196
201
  readonly op: "rotatePage";
197
202
  readonly pageIndex: number;
198
203
  /** Absolute clockwise rotation in degrees. */
199
204
  readonly rotation: 0 | 90 | 180 | 270;
200
- }
205
+ readonly by?: never;
206
+ } | {
207
+ readonly op: "rotatePage";
208
+ readonly pageIndex: number;
209
+ /** Clockwise turn in degrees from the page's current rotation. */
210
+ readonly by: 90 | 180 | 270;
211
+ readonly rotation?: never;
212
+ };
201
213
  /** The PDF operation union; operations are added as they ship. */
202
214
  export type PdfOperation = InsertTextBoxOperation | ReplaceTextOperation | SetTextStyleOperation | ResizeElementOperation | MoveElementOperation | DeleteElementOperation | InsertPageOperation | DeletePageOperation | MovePageOperation | RotatePageOperation | InsertShapeOperation | SetShapeStyleOperation | InsertImageOperation | InsertTableOperation | SetTableCellOperation;
203
215
  export interface PdfSaveOptions extends SaveOptions {
@@ -232,6 +244,41 @@ export interface TextLayoutLine {
232
244
  readonly fontSize: number;
233
245
  readonly color: string;
234
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
+ }
235
282
  /** The drawn geometry of a `text`, `textBox` or `table` element. */
236
283
  export interface TextLayout {
237
284
  readonly elementId: string;
@@ -270,6 +317,11 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
270
317
  save(options?: PdfSaveOptions): Promise<SavedDocument>;
271
318
  /** Lines, glyph boxes and styles of a `text`, `textBox` or `table` element. */
272
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>>;
273
325
  /** The text position nearest to a page-space point; none on a page without text. */
274
326
  positionAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadItem<TextPosition>>;
275
327
  /** The rectangles a range covers, one per line fragment, in reading order. */
@@ -321,7 +373,7 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
321
373
  deletePage(fields: Fields<DeletePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
322
374
  /** Reorders pages. */
323
375
  movePage(fields: Fields<MovePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
324
- /** Sets a page's rotation. */
376
+ /** Sets a page's rotation, or turns it from the one it has. */
325
377
  rotatePage(fields: Fields<RotatePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
326
378
  /** Draws a rectangle, ellipse or line as a path object. */
327
379
  insertShape(fields: Fields<InsertShapeOperation>, options?: ApplyOptions): Promise<EditReceipt>;
@@ -1,5 +1,5 @@
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";
2
+ import type { EditSessionAccess, EditSessionCore } from "../engine.js";
3
3
  import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, SavedDocument, TextTarget } from "../types.js";
4
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";
5
5
  /**
@@ -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>;