web-doc 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +26 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +47 -2
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +51 -0
- package/dist/edit/docx/engine.js +473 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +92 -0
- package/dist/edit/docx/model.js +339 -0
- package/dist/edit/docx/operations.d.ts +37 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +13 -0
- package/dist/edit/docx/provider.js +34 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +37 -0
- package/dist/edit/docx/session.js +426 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +494 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +219 -0
- package/dist/edit/docx/text-ops.d.ts +18 -0
- package/dist/edit/docx/text-ops.js +458 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +263 -0
- package/dist/edit/docx/types.d.ts +191 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +50 -0
- package/dist/edit/docx/write.js +352 -0
- package/dist/edit/engine.d.ts +117 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +53 -0
- package/dist/edit/history.js +101 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +14 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +53 -0
- package/dist/edit/pdf/session.js +273 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +91 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +44 -0
- package/dist/edit/pptx/session.js +114 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +66 -0
- package/dist/edit/session.js +701 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +276 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/limits.js +3 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/ooxml-edit-worker.js +8671 -0
- package/dist/workers/pdf-edit-worker.js +10844 -0
- package/package.json +3 -3
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Fonts for text web-doc writes into a PDF: the standard 14 fonts for
|
|
3
|
+
* WinAnsi text, host-registered TrueType fonts, and a bundled Noto Sans
|
|
4
|
+
* Latin/Cyrillic face as the last resort. Coverage is read from each font's
|
|
5
|
+
* cmap, because PDFium draws a notdef glyph for anything else.
|
|
6
|
+
*/
|
|
7
|
+
const STANDARD_FAMILIES = {
|
|
8
|
+
helvetica: [
|
|
9
|
+
"Helvetica",
|
|
10
|
+
"Helvetica-Bold",
|
|
11
|
+
"Helvetica-Oblique",
|
|
12
|
+
"Helvetica-BoldOblique",
|
|
13
|
+
],
|
|
14
|
+
times: ["Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic"],
|
|
15
|
+
courier: [
|
|
16
|
+
"Courier",
|
|
17
|
+
"Courier-Bold",
|
|
18
|
+
"Courier-Oblique",
|
|
19
|
+
"Courier-BoldOblique",
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
/** Code points WinAnsiEncoding places in 0x80–0x9F. */
|
|
23
|
+
const WIN_ANSI_EXTRAS = new Set("€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ".split("").map((c) => c.codePointAt(0)));
|
|
24
|
+
/** FPDFText_LoadFont font types. */
|
|
25
|
+
const FONT_TRUETYPE = 1;
|
|
26
|
+
export const FALLBACK_FAMILY = "Noto Sans";
|
|
27
|
+
export function isStandardFamily(family) {
|
|
28
|
+
return family.toLowerCase() in STANDARD_FAMILIES;
|
|
29
|
+
}
|
|
30
|
+
/** The standard-14 name for a family and style, e.g. Helvetica + bold. */
|
|
31
|
+
export function standardFontName(family, bold, italic) {
|
|
32
|
+
const variants = STANDARD_FAMILIES[family.toLowerCase()];
|
|
33
|
+
if (!variants)
|
|
34
|
+
return undefined;
|
|
35
|
+
return variants[(bold ? 1 : 0) + (italic ? 2 : 0)];
|
|
36
|
+
}
|
|
37
|
+
/** The first character a standard font cannot encode, if any. */
|
|
38
|
+
export function firstNonWinAnsi(text) {
|
|
39
|
+
for (const character of text) {
|
|
40
|
+
const code = character.codePointAt(0);
|
|
41
|
+
if (code === 0x0a)
|
|
42
|
+
continue;
|
|
43
|
+
if ((code >= 0x20 && code <= 0x7e) ||
|
|
44
|
+
(code >= 0xa0 && code <= 0xff) ||
|
|
45
|
+
WIN_ANSI_EXTRAS.has(code))
|
|
46
|
+
continue;
|
|
47
|
+
return character;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Host-registered fonts and the fallback face, fetched on demand and loaded
|
|
53
|
+
* into whichever PDFium document needs them.
|
|
54
|
+
*/
|
|
55
|
+
export class FontLibrary {
|
|
56
|
+
#registered = [];
|
|
57
|
+
#fetch;
|
|
58
|
+
#bytes = new Map();
|
|
59
|
+
#coverage = new Map();
|
|
60
|
+
/** Loaded PDFium font handles, keyed by document and font bytes. */
|
|
61
|
+
#handles = new Map();
|
|
62
|
+
#fallbackUrl;
|
|
63
|
+
#fallback;
|
|
64
|
+
constructor(fetchBytes) {
|
|
65
|
+
this.#fetch = fetchBytes;
|
|
66
|
+
}
|
|
67
|
+
setFallbackUrl(url) {
|
|
68
|
+
this.#fallbackUrl = url;
|
|
69
|
+
}
|
|
70
|
+
register(fonts) {
|
|
71
|
+
this.#registered.length = 0;
|
|
72
|
+
this.#registered.push(...fonts);
|
|
73
|
+
}
|
|
74
|
+
/** Whether `family` names a registered font. */
|
|
75
|
+
hasFamily(family) {
|
|
76
|
+
return this.#registered.some((font) => sameFamily(font.family, family));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fetches whatever the given texts and families may need, so that
|
|
80
|
+
* validation and drawing can run synchronously afterwards.
|
|
81
|
+
*/
|
|
82
|
+
async prepare(requests) {
|
|
83
|
+
const families = new Set(requests.map((request) => request.family.toLowerCase()));
|
|
84
|
+
const fonts = this.#registered.filter((font) => families.has(font.family.toLowerCase()));
|
|
85
|
+
for (const font of fonts)
|
|
86
|
+
await this.#bytesOf(font);
|
|
87
|
+
// The fallback is only worth fetching for text no registered font covers.
|
|
88
|
+
const needsFallback = requests.some((request) =>
|
|
89
|
+
// Text the standard fonts cannot encode, or a family only the
|
|
90
|
+
// fallback can stand in for (an embedded font that lost a glyph).
|
|
91
|
+
(firstNonWinAnsi(request.text) !== undefined ||
|
|
92
|
+
(!isStandardFamily(request.family) &&
|
|
93
|
+
!this.hasFamily(request.family))) &&
|
|
94
|
+
!this.#coveringRegistered({ ...request, bold: false, italic: false }));
|
|
95
|
+
if (needsFallback && !this.#fallback && this.#fallbackUrl)
|
|
96
|
+
this.#fallback = await this.#fetch(this.#fallbackUrl);
|
|
97
|
+
}
|
|
98
|
+
/** Checks a request against what is loaded; `undefined` means it will resolve. */
|
|
99
|
+
problem(request) {
|
|
100
|
+
const standard = isStandardFamily(request.family);
|
|
101
|
+
if (!standard && !this.hasFamily(request.family))
|
|
102
|
+
return {
|
|
103
|
+
path: "/style/fontFamily",
|
|
104
|
+
code: "unknown-font",
|
|
105
|
+
message: `Unknown font family ${request.family}; Helvetica, Times, Courier and registered fonts are available`,
|
|
106
|
+
};
|
|
107
|
+
if (standard && firstNonWinAnsi(request.text) === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
if (this.#coveringRegistered(request))
|
|
110
|
+
return undefined;
|
|
111
|
+
if (this.#fallback && this.#covers(this.#fallback, request.text))
|
|
112
|
+
return undefined;
|
|
113
|
+
const bad = firstNonWinAnsi(request.text) ?? firstUncovered(request.text, this);
|
|
114
|
+
return {
|
|
115
|
+
path: "/text",
|
|
116
|
+
code: "font-unavailable",
|
|
117
|
+
message: bad
|
|
118
|
+
? `No available font can draw "${bad}" (U+${bad.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")})`
|
|
119
|
+
: "No available font can draw the text",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** The font to draw `request` with in `document`; `prepare` must have run. */
|
|
123
|
+
resolve(pdfium, document, request) {
|
|
124
|
+
const { lib } = pdfium;
|
|
125
|
+
if (isStandardFamily(request.family) &&
|
|
126
|
+
firstNonWinAnsi(request.text) === undefined) {
|
|
127
|
+
const name = standardFontName(request.family, request.bold, request.italic);
|
|
128
|
+
return {
|
|
129
|
+
handle: lib.FPDFText_LoadStandardFont(document, name),
|
|
130
|
+
family: name,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const registered = this.#coveringRegistered(request);
|
|
134
|
+
if (registered) {
|
|
135
|
+
const exact = isBold(registered.font) === request.bold &&
|
|
136
|
+
isItalic(registered.font) === request.italic;
|
|
137
|
+
return {
|
|
138
|
+
handle: this.#load(pdfium, document, registered.bytes),
|
|
139
|
+
family: registered.font.family,
|
|
140
|
+
...(exact
|
|
141
|
+
? {}
|
|
142
|
+
: {
|
|
143
|
+
substitution: `${registered.font.family} has no matching bold or italic face`,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!this.#fallback || !this.#covers(this.#fallback, request.text))
|
|
148
|
+
throw new Error(`No font covers "${request.text}"`);
|
|
149
|
+
return {
|
|
150
|
+
handle: this.#load(pdfium, document, this.#fallback),
|
|
151
|
+
family: FALLBACK_FAMILY,
|
|
152
|
+
substitution: `${request.family} cannot draw this text; ${FALLBACK_FAMILY} is used`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Closes the fonts loaded into a document that is going away. */
|
|
156
|
+
release(pdfium, document) {
|
|
157
|
+
const handles = this.#handles.get(document);
|
|
158
|
+
if (!handles)
|
|
159
|
+
return;
|
|
160
|
+
for (const handle of handles.values())
|
|
161
|
+
pdfium.lib.FPDFFont_Close(handle);
|
|
162
|
+
this.#handles.delete(document);
|
|
163
|
+
}
|
|
164
|
+
covers(bytes, text) {
|
|
165
|
+
return this.#covers(bytes, text);
|
|
166
|
+
}
|
|
167
|
+
#coveringRegistered(request) {
|
|
168
|
+
const candidates = this.#registered
|
|
169
|
+
.filter((font) => sameFamily(font.family, request.family))
|
|
170
|
+
.map((font) => ({ font, bytes: this.#loadedBytes(font) }))
|
|
171
|
+
.filter((entry) => entry.bytes !== undefined && this.#covers(entry.bytes, request.text));
|
|
172
|
+
// The closest style wins: matching bold and italic, then matching one.
|
|
173
|
+
const score = (font) => (isBold(font) === request.bold ? 2 : 0) +
|
|
174
|
+
(isItalic(font) === request.italic ? 1 : 0);
|
|
175
|
+
return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
|
|
176
|
+
}
|
|
177
|
+
#covers(bytes, text) {
|
|
178
|
+
let coverage = this.#coverage.get(bytes);
|
|
179
|
+
if (!coverage) {
|
|
180
|
+
coverage = parseCmap(bytes);
|
|
181
|
+
this.#coverage.set(bytes, coverage);
|
|
182
|
+
}
|
|
183
|
+
for (const character of text) {
|
|
184
|
+
const code = character.codePointAt(0);
|
|
185
|
+
if (code === 0x0a || code === 0x20)
|
|
186
|
+
continue;
|
|
187
|
+
if (!coverage.has(code))
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
#load(pdfium, document, bytes) {
|
|
193
|
+
let handles = this.#handles.get(document);
|
|
194
|
+
if (!handles) {
|
|
195
|
+
handles = new Map();
|
|
196
|
+
this.#handles.set(document, handles);
|
|
197
|
+
}
|
|
198
|
+
let handle = handles.get(bytes);
|
|
199
|
+
if (handle === undefined) {
|
|
200
|
+
const pointer = pdfium.writeBytes(bytes);
|
|
201
|
+
try {
|
|
202
|
+
handle = pdfium.lib.FPDFText_LoadFont(document, pointer, bytes.byteLength, FONT_TRUETYPE, true);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
pdfium.free(pointer);
|
|
206
|
+
}
|
|
207
|
+
if (!handle)
|
|
208
|
+
throw new Error("PDFium could not load the font");
|
|
209
|
+
handles.set(bytes, handle);
|
|
210
|
+
}
|
|
211
|
+
return handle;
|
|
212
|
+
}
|
|
213
|
+
#loadedBytes(font) {
|
|
214
|
+
return typeof font.source === "string"
|
|
215
|
+
? this.#bytes.get(font.source)
|
|
216
|
+
: (this.#bytes.get(sourceKey(font)) ?? new Uint8Array(font.source));
|
|
217
|
+
}
|
|
218
|
+
async #bytesOf(font) {
|
|
219
|
+
const key = sourceKey(font);
|
|
220
|
+
let bytes = this.#bytes.get(key);
|
|
221
|
+
if (!bytes) {
|
|
222
|
+
bytes =
|
|
223
|
+
typeof font.source === "string"
|
|
224
|
+
? await this.#fetch(font.source)
|
|
225
|
+
: new Uint8Array(font.source);
|
|
226
|
+
this.#bytes.set(key, bytes);
|
|
227
|
+
}
|
|
228
|
+
return bytes;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function sourceKey(font) {
|
|
232
|
+
return typeof font.source === "string"
|
|
233
|
+
? font.source
|
|
234
|
+
: `${font.family}:${font.weight}:${font.style}:${font.source.byteLength}`;
|
|
235
|
+
}
|
|
236
|
+
function sameFamily(a, b) {
|
|
237
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
238
|
+
}
|
|
239
|
+
function isBold(font) {
|
|
240
|
+
return font.weight >= 600;
|
|
241
|
+
}
|
|
242
|
+
function isItalic(font) {
|
|
243
|
+
return font.style !== "normal";
|
|
244
|
+
}
|
|
245
|
+
function firstUncovered(text, library) {
|
|
246
|
+
void library;
|
|
247
|
+
for (const character of text)
|
|
248
|
+
if (character !== "\n" && character !== " ")
|
|
249
|
+
return character;
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
/** The code points a TrueType font maps to glyphs, from its cmap table. */
|
|
253
|
+
/**
|
|
254
|
+
* Whether glyphs of a TrueType font have outline data: a subset font can keep
|
|
255
|
+
* a cmap entry for a glyph it emptied. Returns undefined without a glyf table.
|
|
256
|
+
*/
|
|
257
|
+
function glyphOutlines(view, tables) {
|
|
258
|
+
const head = tables.get("head");
|
|
259
|
+
const maxp = tables.get("maxp");
|
|
260
|
+
const loca = tables.get("loca");
|
|
261
|
+
if (!head || !maxp || !loca || !tables.has("glyf"))
|
|
262
|
+
return undefined;
|
|
263
|
+
const longOffsets = view.getInt16(head.offset + 50) === 1;
|
|
264
|
+
const count = view.getUint16(maxp.offset + 4);
|
|
265
|
+
return (glyph) => {
|
|
266
|
+
if (glyph < 0 || glyph >= count)
|
|
267
|
+
return false;
|
|
268
|
+
const at = loca.offset + glyph * (longOffsets ? 4 : 2);
|
|
269
|
+
const start = longOffsets ? view.getUint32(at) : view.getUint16(at) * 2;
|
|
270
|
+
const end = longOffsets
|
|
271
|
+
? view.getUint32(at + 4)
|
|
272
|
+
: view.getUint16(at + 2) * 2;
|
|
273
|
+
return end > start;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
export function parseCmap(bytes) {
|
|
277
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
278
|
+
const ranges = [];
|
|
279
|
+
/** Glyph ids by code point, for the outline check; filled for format 4 and 12. */
|
|
280
|
+
const glyphs = new Map();
|
|
281
|
+
let outlines;
|
|
282
|
+
try {
|
|
283
|
+
const tables = view.getUint16(4);
|
|
284
|
+
let cmapOffset = -1;
|
|
285
|
+
const offsets = new Map();
|
|
286
|
+
for (let index = 0; index < tables; index += 1) {
|
|
287
|
+
const record = 12 + index * 16;
|
|
288
|
+
const tag = String.fromCharCode(bytes[record], bytes[record + 1], bytes[record + 2], bytes[record + 3]);
|
|
289
|
+
offsets.set(tag, {
|
|
290
|
+
offset: view.getUint32(record + 8),
|
|
291
|
+
length: view.getUint32(record + 12),
|
|
292
|
+
});
|
|
293
|
+
if (tag === "cmap")
|
|
294
|
+
cmapOffset = view.getUint32(record + 8);
|
|
295
|
+
}
|
|
296
|
+
outlines = glyphOutlines(view, offsets);
|
|
297
|
+
if (cmapOffset < 0)
|
|
298
|
+
return {
|
|
299
|
+
has: () => false,
|
|
300
|
+
drawable: () => false,
|
|
301
|
+
glyph: () => undefined,
|
|
302
|
+
};
|
|
303
|
+
const subtables = view.getUint16(cmapOffset + 2);
|
|
304
|
+
let best;
|
|
305
|
+
for (let index = 0; index < subtables; index += 1) {
|
|
306
|
+
const record = cmapOffset + 4 + index * 8;
|
|
307
|
+
const platform = view.getUint16(record);
|
|
308
|
+
const encoding = view.getUint16(record + 2);
|
|
309
|
+
const offset = cmapOffset + view.getUint32(record + 4);
|
|
310
|
+
const format = view.getUint16(offset);
|
|
311
|
+
const unicode = platform === 0 ||
|
|
312
|
+
(platform === 3 && (encoding === 1 || encoding === 10));
|
|
313
|
+
if (!unicode || (format !== 4 && format !== 12))
|
|
314
|
+
continue;
|
|
315
|
+
if (!best || format > best.format)
|
|
316
|
+
best = { offset, format };
|
|
317
|
+
}
|
|
318
|
+
if (!best)
|
|
319
|
+
return {
|
|
320
|
+
has: () => false,
|
|
321
|
+
drawable: () => false,
|
|
322
|
+
glyph: () => undefined,
|
|
323
|
+
};
|
|
324
|
+
if (best.format === 12) {
|
|
325
|
+
const groups = view.getUint32(best.offset + 12);
|
|
326
|
+
for (let index = 0; index < groups; index += 1) {
|
|
327
|
+
const group = best.offset + 16 + index * 12;
|
|
328
|
+
const start = view.getUint32(group);
|
|
329
|
+
const end = view.getUint32(group + 4);
|
|
330
|
+
const startGlyph = view.getUint32(group + 8);
|
|
331
|
+
ranges.push([start, end]);
|
|
332
|
+
for (let code = start; code <= end && code - start < 0x10000; code += 1)
|
|
333
|
+
glyphs.set(code, startGlyph + (code - start));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const segments = view.getUint16(best.offset + 6) / 2;
|
|
338
|
+
const ends = best.offset + 14;
|
|
339
|
+
const starts = ends + segments * 2 + 2;
|
|
340
|
+
const deltas = starts + segments * 2;
|
|
341
|
+
const rangeOffsets = deltas + segments * 2;
|
|
342
|
+
for (let index = 0; index < segments; index += 1) {
|
|
343
|
+
const end = view.getUint16(ends + index * 2);
|
|
344
|
+
const start = view.getUint16(starts + index * 2);
|
|
345
|
+
if (start === 0xffff)
|
|
346
|
+
continue;
|
|
347
|
+
ranges.push([start, end]);
|
|
348
|
+
const delta = view.getInt16(deltas + index * 2);
|
|
349
|
+
const rangeOffset = view.getUint16(rangeOffsets + index * 2);
|
|
350
|
+
for (let code = start; code <= end; code += 1) {
|
|
351
|
+
let glyph;
|
|
352
|
+
if (rangeOffset === 0)
|
|
353
|
+
glyph = (code + delta) & 0xffff;
|
|
354
|
+
else {
|
|
355
|
+
const at = rangeOffsets + index * 2 + rangeOffset + (code - start) * 2;
|
|
356
|
+
const raw = view.getUint16(at);
|
|
357
|
+
glyph = raw === 0 ? 0 : (raw + delta) & 0xffff;
|
|
358
|
+
}
|
|
359
|
+
glyphs.set(code, glyph);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return { has: () => false, drawable: () => false, glyph: () => undefined };
|
|
366
|
+
}
|
|
367
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
368
|
+
return {
|
|
369
|
+
glyph(codePoint) {
|
|
370
|
+
return glyphs.get(codePoint);
|
|
371
|
+
},
|
|
372
|
+
drawable(codePoint) {
|
|
373
|
+
const glyph = glyphs.get(codePoint);
|
|
374
|
+
if (glyph === undefined || glyph === 0)
|
|
375
|
+
return false;
|
|
376
|
+
// Without a glyf table (CFF outlines) a mapped glyph is taken as drawn.
|
|
377
|
+
return outlines ? outlines(glyph) : true;
|
|
378
|
+
},
|
|
379
|
+
has(codePoint) {
|
|
380
|
+
let low = 0;
|
|
381
|
+
let high = ranges.length - 1;
|
|
382
|
+
while (low <= high) {
|
|
383
|
+
const middle = (low + high) >> 1;
|
|
384
|
+
const [start, end] = ranges[middle];
|
|
385
|
+
if (codePoint < start)
|
|
386
|
+
high = middle - 1;
|
|
387
|
+
else if (codePoint > end)
|
|
388
|
+
low = middle + 1;
|
|
389
|
+
else
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Measures text with PDFium's own metrics by building throw-away text
|
|
398
|
+
* objects, so layout agrees with what PDFium draws. Advances are cached per
|
|
399
|
+
* character and font at 1000 pt and scale linearly.
|
|
400
|
+
*/
|
|
401
|
+
export class TextMeasurer {
|
|
402
|
+
#pdfium;
|
|
403
|
+
#document;
|
|
404
|
+
#advances = new Map();
|
|
405
|
+
#metrics = new Map();
|
|
406
|
+
constructor(pdfium, document) {
|
|
407
|
+
this.#pdfium = pdfium;
|
|
408
|
+
this.#document = document;
|
|
409
|
+
}
|
|
410
|
+
/** Width of `text` in points at `fontSize`, without kerning. */
|
|
411
|
+
advance(font, fontSize, text) {
|
|
412
|
+
let total = 0;
|
|
413
|
+
for (const character of text)
|
|
414
|
+
total += this.#advanceOf(font, character);
|
|
415
|
+
return (total * fontSize) / 1000;
|
|
416
|
+
}
|
|
417
|
+
/** Ascent above and descent below the baseline, in points, both positive. */
|
|
418
|
+
metrics(font, fontSize) {
|
|
419
|
+
let unit = this.#metrics.get(font);
|
|
420
|
+
if (!unit) {
|
|
421
|
+
const { lib } = this.#pdfium;
|
|
422
|
+
const ascent = this.#pdfium.readNumbers(1, "float", ([pointer]) => lib.FPDFFont_GetAscent(font, 1000, pointer))?.[0] ?? 750;
|
|
423
|
+
const descent = this.#pdfium.readNumbers(1, "float", ([pointer]) => lib.FPDFFont_GetDescent(font, 1000, pointer))?.[0] ?? -250;
|
|
424
|
+
unit = { ascent, descent: Math.abs(descent) };
|
|
425
|
+
this.#metrics.set(font, unit);
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
ascent: (unit.ascent * fontSize) / 1000,
|
|
429
|
+
descent: (unit.descent * fontSize) / 1000,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
#advanceOf(font, character) {
|
|
433
|
+
let cache = this.#advances.get(font);
|
|
434
|
+
if (!cache) {
|
|
435
|
+
cache = new Map();
|
|
436
|
+
this.#advances.set(font, cache);
|
|
437
|
+
}
|
|
438
|
+
let advance = cache.get(character);
|
|
439
|
+
if (advance === undefined) {
|
|
440
|
+
// Ink boxes do not give advances, but the box of "H?H" minus the box
|
|
441
|
+
// of "HH" is exactly the advance of "?", for any "?" including space.
|
|
442
|
+
advance =
|
|
443
|
+
this.#inkWidth(font, `H${character}H`) - this.#inkWidth(font, "HH");
|
|
444
|
+
cache.set(character, advance);
|
|
445
|
+
}
|
|
446
|
+
return advance;
|
|
447
|
+
}
|
|
448
|
+
#inkWidth(font, text) {
|
|
449
|
+
const { lib } = this.#pdfium;
|
|
450
|
+
const object = lib.FPDFPageObj_CreateTextObj(this.#document, font, 1000);
|
|
451
|
+
try {
|
|
452
|
+
const wide = this.#pdfium.writeWideString(text);
|
|
453
|
+
try {
|
|
454
|
+
lib.FPDFText_SetText(object, wide);
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
this.#pdfium.free(wide);
|
|
458
|
+
}
|
|
459
|
+
const bounds = this.#pdfium.readNumbers(4, "float", ([l, b, r, t]) => lib.FPDFPageObj_GetBounds(object, l, b, r, t));
|
|
460
|
+
return bounds ? bounds[2] - bounds[0] : 0;
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
lib.FPDFPageObj_Destroy(object);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PagePoint, PageRect } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* How a page is displayed: its visible box in PDF user space (media box ∩
|
|
4
|
+
* crop box, y up) and its /Rotate quarter turns. Page space is the displayed
|
|
5
|
+
* page in points with the origin at its top-left corner and y down — the same
|
|
6
|
+
* frame PDF.js uses for its viewport at scale 1.
|
|
7
|
+
*/
|
|
8
|
+
export interface PageGeometry {
|
|
9
|
+
/** User-space box: left and bottom are the minimum x and y. */
|
|
10
|
+
readonly box: {
|
|
11
|
+
readonly left: number;
|
|
12
|
+
readonly bottom: number;
|
|
13
|
+
readonly right: number;
|
|
14
|
+
readonly top: number;
|
|
15
|
+
};
|
|
16
|
+
/** Quarter turns clockwise, 0–3. */
|
|
17
|
+
readonly rotation: number;
|
|
18
|
+
}
|
|
19
|
+
/** Displayed size in points, rotation applied. */
|
|
20
|
+
export declare function displayedSize(geometry: PageGeometry): {
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function userToPage(geometry: PageGeometry, x: number, y: number): PagePoint;
|
|
25
|
+
export declare function pageToUser(geometry: PageGeometry, x: number, y: number): {
|
|
26
|
+
readonly x: number;
|
|
27
|
+
readonly y: number;
|
|
28
|
+
};
|
|
29
|
+
/** Axis-aligned page-space box of a user-space rectangle. */
|
|
30
|
+
export declare function userRectToPage(geometry: PageGeometry, left: number, bottom: number, right: number, top: number): PageRect;
|
|
31
|
+
export declare function unionRects(rects: readonly PageRect[]): PageRect;
|
|
32
|
+
export declare function rectContains(rect: PageRect, point: PagePoint): boolean;
|
|
33
|
+
export declare function rectsIntersect(a: PageRect, b: PageRect): boolean;
|
|
34
|
+
/** Rounds geometry to a thousandth of a point so results are tidy and stable. */
|
|
35
|
+
export declare function roundRect(rect: PageRect): PageRect;
|
|
36
|
+
export declare function round(value: number): number;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Displayed size in points, rotation applied. */
|
|
2
|
+
export function displayedSize(geometry) {
|
|
3
|
+
const width = geometry.box.right - geometry.box.left;
|
|
4
|
+
const height = geometry.box.top - geometry.box.bottom;
|
|
5
|
+
return geometry.rotation % 2 === 0
|
|
6
|
+
? { width, height }
|
|
7
|
+
: { width: height, height: width };
|
|
8
|
+
}
|
|
9
|
+
export function userToPage(geometry, x, y) {
|
|
10
|
+
const width = geometry.box.right - geometry.box.left;
|
|
11
|
+
const height = geometry.box.top - geometry.box.bottom;
|
|
12
|
+
// Unrotated page space: flip y so it grows downwards from the top edge.
|
|
13
|
+
const px = x - geometry.box.left;
|
|
14
|
+
const py = geometry.box.top - y;
|
|
15
|
+
switch (geometry.rotation) {
|
|
16
|
+
case 1:
|
|
17
|
+
return { x: height - py, y: px };
|
|
18
|
+
case 2:
|
|
19
|
+
return { x: width - px, y: height - py };
|
|
20
|
+
case 3:
|
|
21
|
+
return { x: py, y: width - px };
|
|
22
|
+
default:
|
|
23
|
+
return { x: px, y: py };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function pageToUser(geometry, x, y) {
|
|
27
|
+
const width = geometry.box.right - geometry.box.left;
|
|
28
|
+
const height = geometry.box.top - geometry.box.bottom;
|
|
29
|
+
let px;
|
|
30
|
+
let py;
|
|
31
|
+
switch (geometry.rotation) {
|
|
32
|
+
case 1:
|
|
33
|
+
px = y;
|
|
34
|
+
py = height - x;
|
|
35
|
+
break;
|
|
36
|
+
case 2:
|
|
37
|
+
px = width - x;
|
|
38
|
+
py = height - y;
|
|
39
|
+
break;
|
|
40
|
+
case 3:
|
|
41
|
+
px = width - y;
|
|
42
|
+
py = x;
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
px = x;
|
|
46
|
+
py = y;
|
|
47
|
+
}
|
|
48
|
+
return { x: px + geometry.box.left, y: geometry.box.top - py };
|
|
49
|
+
}
|
|
50
|
+
/** Axis-aligned page-space box of a user-space rectangle. */
|
|
51
|
+
export function userRectToPage(geometry, left, bottom, right, top) {
|
|
52
|
+
const corners = [
|
|
53
|
+
userToPage(geometry, left, bottom),
|
|
54
|
+
userToPage(geometry, right, bottom),
|
|
55
|
+
userToPage(geometry, right, top),
|
|
56
|
+
userToPage(geometry, left, top),
|
|
57
|
+
];
|
|
58
|
+
const xs = corners.map((corner) => corner.x);
|
|
59
|
+
const ys = corners.map((corner) => corner.y);
|
|
60
|
+
const x = Math.min(...xs);
|
|
61
|
+
const y = Math.min(...ys);
|
|
62
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
63
|
+
}
|
|
64
|
+
export function unionRects(rects) {
|
|
65
|
+
const x = Math.min(...rects.map((rect) => rect.x));
|
|
66
|
+
const y = Math.min(...rects.map((rect) => rect.y));
|
|
67
|
+
return {
|
|
68
|
+
x,
|
|
69
|
+
y,
|
|
70
|
+
width: Math.max(...rects.map((rect) => rect.x + rect.width)) - x,
|
|
71
|
+
height: Math.max(...rects.map((rect) => rect.y + rect.height)) - y,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function rectContains(rect, point) {
|
|
75
|
+
return (point.x >= rect.x &&
|
|
76
|
+
point.x <= rect.x + rect.width &&
|
|
77
|
+
point.y >= rect.y &&
|
|
78
|
+
point.y <= rect.y + rect.height);
|
|
79
|
+
}
|
|
80
|
+
export function rectsIntersect(a, b) {
|
|
81
|
+
return !(a.x + a.width < b.x ||
|
|
82
|
+
b.x + b.width < a.x ||
|
|
83
|
+
a.y + a.height < b.y ||
|
|
84
|
+
b.y + b.height < a.y);
|
|
85
|
+
}
|
|
86
|
+
/** Rounds geometry to a thousandth of a point so results are tidy and stable. */
|
|
87
|
+
export function roundRect(rect) {
|
|
88
|
+
return {
|
|
89
|
+
x: round(rect.x),
|
|
90
|
+
y: round(rect.y),
|
|
91
|
+
width: round(rect.width),
|
|
92
|
+
height: round(rect.height),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function round(value) {
|
|
96
|
+
return Math.round(value * 1000) / 1000;
|
|
97
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WorkerOperationHandler } from "../../../worker-endpoint.js";
|
|
2
|
+
import { pdfOperationSchemas } from "../schemas.js";
|
|
3
|
+
import type { ImageDecoder } from "./images.js";
|
|
4
|
+
import type { Pdfium } from "./pdfium.js";
|
|
5
|
+
export interface PdfEditHost {
|
|
6
|
+
/** Instantiates PDFium for a WASM URL. */
|
|
7
|
+
loadPdfium(wasmUrl: string): Promise<Pdfium>;
|
|
8
|
+
/** The compaction pass of a full save; tests inject a failing one. */
|
|
9
|
+
readonly compact?: (bytes: Uint8Array) => Uint8Array;
|
|
10
|
+
/** Fetches font bytes for a URL. */
|
|
11
|
+
fetchBytes(url: string): Promise<Uint8Array>;
|
|
12
|
+
/** Decodes an image into RGBA pixels. */
|
|
13
|
+
decodeImage: ImageDecoder;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Serves the edit worker protocol for one PDF document. It runs inside the
|
|
17
|
+
* edit worker in the browser and directly in Node tests; the host supplies
|
|
18
|
+
* how WebAssembly and fonts are obtained.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createPdfEditHandler(host: PdfEditHost): WorkerOperationHandler;
|
|
21
|
+
export { pdfOperationSchemas };
|