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
|
@@ -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
|
-
|
|
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,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
|
}
|
package/dist/edit/pdf/schemas.js
CHANGED
|
@@ -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
|
-
|
|
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>>;
|
package/dist/edit/pdf/session.js
CHANGED
|
@@ -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
|
}
|
package/dist/edit/pdf/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|