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,424 @@
|
|
|
1
|
+
import { objectBounds, OBJECT_TEXT } from "./elements.js";
|
|
2
|
+
import { firstNonWinAnsi, isStandardFamily, parseCmap, } from "./fonts.js";
|
|
3
|
+
import { pageToUser } from "./geometry.js";
|
|
4
|
+
import { parseColor, setText, textBoxReplaceText, textBoxSetTextStyle, textBoxTarget, } from "./text-box.js";
|
|
5
|
+
/*
|
|
6
|
+
* Edits to text objects that already exist in the file. The object is changed
|
|
7
|
+
* in place only when its font provably covers the new text; otherwise it is
|
|
8
|
+
* replaced by a new object in a font that does, at the same place, size and
|
|
9
|
+
* colour, and the receipt says so.
|
|
10
|
+
*/
|
|
11
|
+
export const replaceText = {
|
|
12
|
+
validate(operation, context, issue) {
|
|
13
|
+
const box = textBoxTarget(operation.target, context);
|
|
14
|
+
if (box) {
|
|
15
|
+
const spliced = splice(box.spec.text, operation, issue);
|
|
16
|
+
if (spliced === undefined)
|
|
17
|
+
return;
|
|
18
|
+
return textBoxReplaceText.validate(wholeTextOperation(operation, spliced), context, issue);
|
|
19
|
+
}
|
|
20
|
+
const target = textTarget(operation.target, context, issue);
|
|
21
|
+
if (!target)
|
|
22
|
+
return;
|
|
23
|
+
const whole = splice(target.element.text ?? "", operation, issue);
|
|
24
|
+
if (whole === undefined)
|
|
25
|
+
return;
|
|
26
|
+
if (!validateScript(operation.text, issue))
|
|
27
|
+
return;
|
|
28
|
+
if (canKeepFont(context, target, whole))
|
|
29
|
+
return;
|
|
30
|
+
const request = {
|
|
31
|
+
family: target.element.textStyle?.fontFamily ?? "Helvetica",
|
|
32
|
+
bold: target.element.textStyle?.bold ?? false,
|
|
33
|
+
italic: target.element.textStyle?.italic ?? false,
|
|
34
|
+
text: operation.text,
|
|
35
|
+
};
|
|
36
|
+
// The file's own family is only a hint: what matters is whether some
|
|
37
|
+
// available font can draw the new text.
|
|
38
|
+
let problem = context.fonts.problem(request);
|
|
39
|
+
if (problem?.code === "unknown-font")
|
|
40
|
+
problem = context.fonts.problem({ ...request, family: "Helvetica" });
|
|
41
|
+
if (problem)
|
|
42
|
+
issue(problem.path, problem.code, problem.message);
|
|
43
|
+
},
|
|
44
|
+
apply(operation, context) {
|
|
45
|
+
const box = textBoxTarget(operation.target, context);
|
|
46
|
+
if (box)
|
|
47
|
+
return textBoxReplaceText.apply(wholeTextOperation(operation, splice(box.spec.text, operation)), context);
|
|
48
|
+
const target = textTarget(operation.target, context);
|
|
49
|
+
const previous = target.element.text ?? "";
|
|
50
|
+
const whole = splice(previous, operation);
|
|
51
|
+
if (canKeepFont(context, target, whole)) {
|
|
52
|
+
const kept = replaceInPlace(context, target, whole);
|
|
53
|
+
if (kept)
|
|
54
|
+
return result(target.location);
|
|
55
|
+
}
|
|
56
|
+
const span = spanOf(operation, previous);
|
|
57
|
+
const before = previous.slice(0, span.start);
|
|
58
|
+
const after = previous.slice(span.end);
|
|
59
|
+
if (!before && !after)
|
|
60
|
+
return replaceWithFallback(context, target, operation.text);
|
|
61
|
+
return (splitAround(context, target, before, operation.text, after) ??
|
|
62
|
+
replaceWithFallback(context, target, whole));
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
/** The same operation as a whole-text replacement with `text`. */
|
|
66
|
+
function wholeTextOperation(operation, text) {
|
|
67
|
+
const { range: _range, ...rest } = operation;
|
|
68
|
+
return { ...rest, text };
|
|
69
|
+
}
|
|
70
|
+
/** The target's text with the operation's range replaced; the whole text without one. */
|
|
71
|
+
function splice(previous, operation, issue = () => { }) {
|
|
72
|
+
const { range } = operation;
|
|
73
|
+
if (!range)
|
|
74
|
+
return operation.text;
|
|
75
|
+
const onTarget = (position) => position.elementId === operation.target &&
|
|
76
|
+
Number.isInteger(position.offset) &&
|
|
77
|
+
position.offset >= 0 &&
|
|
78
|
+
position.offset <= previous.length;
|
|
79
|
+
if (!onTarget(range.start) ||
|
|
80
|
+
!onTarget(range.end) ||
|
|
81
|
+
range.start.offset > range.end.offset) {
|
|
82
|
+
issue("/range", "invalid-range", `The range must lie inside the target's text (0–${previous.length})`);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { start, end } = spanOf(operation, previous);
|
|
86
|
+
return previous.slice(0, start) + operation.text + previous.slice(end);
|
|
87
|
+
}
|
|
88
|
+
function spanOf(operation, previous) {
|
|
89
|
+
if (!operation.range)
|
|
90
|
+
return { start: 0, end: previous.length };
|
|
91
|
+
return {
|
|
92
|
+
start: operation.range.start.offset,
|
|
93
|
+
end: operation.range.end.offset,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export const setTextStyle = {
|
|
97
|
+
validate(operation, context, issue) {
|
|
98
|
+
if (textBoxTarget(operation.target, context))
|
|
99
|
+
return textBoxSetTextStyle.validate(operation, context, issue);
|
|
100
|
+
if (!textTarget(operation.target, context, issue))
|
|
101
|
+
return;
|
|
102
|
+
for (const field of [
|
|
103
|
+
"fontFamily",
|
|
104
|
+
"bold",
|
|
105
|
+
"italic",
|
|
106
|
+
"align",
|
|
107
|
+
"lineHeight",
|
|
108
|
+
])
|
|
109
|
+
if (operation.style[field] !== undefined)
|
|
110
|
+
issue(`/style/${field}`, "unsupported-style", `Only color and fontSize can change on existing text; ${field} cannot`);
|
|
111
|
+
},
|
|
112
|
+
apply(operation, context) {
|
|
113
|
+
if (textBoxTarget(operation.target, context))
|
|
114
|
+
return textBoxSetTextStyle.apply(operation, context);
|
|
115
|
+
const target = textTarget(operation.target, context);
|
|
116
|
+
const { lib } = context.pdfium;
|
|
117
|
+
const { location } = target;
|
|
118
|
+
if (operation.style.color !== undefined) {
|
|
119
|
+
const [r, g, b] = parseColor(operation.style.color);
|
|
120
|
+
context.withPage(location.pageIndex, (page) => {
|
|
121
|
+
lib.FPDFPageObj_SetFillColor(lib.FPDFPage_GetObject(page, location.indexes[0]), r, g, b, 255);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (operation.style.fontSize !== undefined)
|
|
125
|
+
resize(context, target, operation.style.fontSize);
|
|
126
|
+
return result(location);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
function textTarget(target, context, issue = () => { }) {
|
|
130
|
+
const location = context.locate(target);
|
|
131
|
+
const element = location && context.element(target);
|
|
132
|
+
if (!location || !element) {
|
|
133
|
+
issue("/target", "unknown-target", `No element ${target}`);
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
if (element.kind !== "text" || location.record.type !== OBJECT_TEXT) {
|
|
137
|
+
issue("/target", "unsupported-target", "Only text objects and text boxes carry text");
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return { location, element };
|
|
141
|
+
}
|
|
142
|
+
/** Right-to-left scripts need shaping the MVP does not do. */
|
|
143
|
+
function validateScript(text, issue) {
|
|
144
|
+
for (const character of text) {
|
|
145
|
+
const code = character.codePointAt(0);
|
|
146
|
+
if ((code >= 0x0590 && code <= 0x08ff) ||
|
|
147
|
+
(code >= 0xfb1d && code <= 0xfdff) ||
|
|
148
|
+
(code >= 0xfe70 && code <= 0xfeff)) {
|
|
149
|
+
issue("/text", "unsupported-script", `Right-to-left text such as "${character}" cannot be edited yet`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether the object's own font can draw `text`: a standard font for WinAnsi
|
|
157
|
+
* text, or an embedded TrueType program whose cmap covers every character
|
|
158
|
+
* (which also answers for subsets, since their cmaps hold only what they
|
|
159
|
+
* kept).
|
|
160
|
+
*/
|
|
161
|
+
function canKeepFont(context, target, text) {
|
|
162
|
+
const { pdfium } = context;
|
|
163
|
+
const { lib } = pdfium;
|
|
164
|
+
return context.withPage(target.location.pageIndex, (page) => {
|
|
165
|
+
const object = lib.FPDFPage_GetObject(page, target.location.indexes[0]);
|
|
166
|
+
const font = lib.FPDFTextObj_GetFont(object);
|
|
167
|
+
if (!lib.FPDFFont_GetIsEmbedded(font)) {
|
|
168
|
+
const base = pdfium.readUtf8String((buffer, bytes) => lib.FPDFFont_GetBaseFontName(font, buffer, bytes));
|
|
169
|
+
return (isStandardFamily(base.split(/[-,]/)[0] ?? "") &&
|
|
170
|
+
firstNonWinAnsi(text) === undefined);
|
|
171
|
+
}
|
|
172
|
+
const coverage = fontCoverage(pdfium, font);
|
|
173
|
+
if (!coverage)
|
|
174
|
+
return false;
|
|
175
|
+
for (const character of text) {
|
|
176
|
+
const code = character.codePointAt(0);
|
|
177
|
+
if (code !== 0x0a && code !== 0x20 && !coverage.drawable(code))
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function fontCoverage(pdfium, font) {
|
|
184
|
+
const { lib } = pdfium;
|
|
185
|
+
const size = pdfium.readNumbers(1, "i32", ([out]) => lib.FPDFFont_GetFontData(font, 0, 0, out))?.[0];
|
|
186
|
+
if (!size)
|
|
187
|
+
return undefined;
|
|
188
|
+
const buffer = pdfium.malloc(size);
|
|
189
|
+
try {
|
|
190
|
+
const out = pdfium.malloc(4);
|
|
191
|
+
try {
|
|
192
|
+
if (!lib.FPDFFont_GetFontData(font, buffer, size, out))
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
pdfium.free(out);
|
|
197
|
+
}
|
|
198
|
+
const data = pdfium.readBytes(buffer, size);
|
|
199
|
+
const tag = String.fromCharCode(data[0], data[1], data[2], data[3]);
|
|
200
|
+
const truetype = (data[0] === 0 && data[1] === 1 && data[2] === 0 && data[3] === 0) ||
|
|
201
|
+
tag === "true";
|
|
202
|
+
return truetype ? parseCmap(data) : undefined;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
pdfium.free(buffer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Sets the text and confirms PDFium reads it back; false means it must be replaced. */
|
|
209
|
+
function replaceInPlace(context, target, text) {
|
|
210
|
+
const { pdfium } = context;
|
|
211
|
+
const { lib } = pdfium;
|
|
212
|
+
const index = target.location.indexes[0];
|
|
213
|
+
const previous = target.element.text ?? "";
|
|
214
|
+
context.withPage(target.location.pageIndex, (page) => {
|
|
215
|
+
setText(pdfium, lib.FPDFPage_GetObject(page, index), text);
|
|
216
|
+
});
|
|
217
|
+
const readBack = context.withPage(target.location.pageIndex, (page) => {
|
|
218
|
+
const textPage = lib.FPDFText_LoadPage(page);
|
|
219
|
+
try {
|
|
220
|
+
return pdfium.readWideString((buffer, bytes) => lib.FPDFTextObj_GetText(lib.FPDFPage_GetObject(page, index), textPage, buffer, bytes));
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
lib.FPDFText_ClosePage(textPage);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
if (readBack === text)
|
|
227
|
+
return true;
|
|
228
|
+
// Put the old text back so the fallback path starts from a known state.
|
|
229
|
+
context.withPage(target.location.pageIndex, (page) => {
|
|
230
|
+
setText(pdfium, lib.FPDFPage_GetObject(page, index), previous);
|
|
231
|
+
});
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Splits the object around a replaced span: the parts before and after keep
|
|
236
|
+
* the object's font, size, colour and baseline, the middle part is drawn in
|
|
237
|
+
* a font that covers it, and the parts follow each other by their advances.
|
|
238
|
+
* The first part keeps the element's id. Returns nothing when a part cannot
|
|
239
|
+
* be written back in the original font, leaving the page as it was.
|
|
240
|
+
*/
|
|
241
|
+
function splitAround(context, target, before, middle, after) {
|
|
242
|
+
const { pdfium, measurer } = context;
|
|
243
|
+
const { lib } = pdfium;
|
|
244
|
+
const { location, element } = target;
|
|
245
|
+
const style = element.textStyle;
|
|
246
|
+
const index = location.indexes[0];
|
|
247
|
+
const fallback = context.fonts.resolve(pdfium, context.document, {
|
|
248
|
+
family: style.fontFamily,
|
|
249
|
+
bold: style.bold,
|
|
250
|
+
italic: style.italic,
|
|
251
|
+
text: middle,
|
|
252
|
+
});
|
|
253
|
+
const createdIds = [];
|
|
254
|
+
const written = context.withPage(location.pageIndex, (page) => {
|
|
255
|
+
const old = lib.FPDFPage_GetObject(page, index);
|
|
256
|
+
const oldFont = lib.FPDFTextObj_GetFont(old);
|
|
257
|
+
const matrix = pdfium.readNumbers(6, "float", ([pointer]) => lib.FPDFPageObj_GetMatrix(old, pointer)) ?? [1, 0, 0, 1, 0, 0];
|
|
258
|
+
const size = pdfium.readNumbers(1, "float", ([pointer]) => lib.FPDFTextObj_GetFontSize(old, pointer))?.[0] ?? style.fontSize;
|
|
259
|
+
const [a, b, c, d, e, f] = matrix;
|
|
260
|
+
const [r, g, bl] = parseColor(style.color);
|
|
261
|
+
const parts = [
|
|
262
|
+
{ text: before, font: oldFont },
|
|
263
|
+
{ text: middle, font: fallback.handle },
|
|
264
|
+
{ text: after, font: oldFont },
|
|
265
|
+
].filter((part) => part.text.length > 0);
|
|
266
|
+
const objects = [];
|
|
267
|
+
let cursor = 0;
|
|
268
|
+
for (const part of parts) {
|
|
269
|
+
const object = lib.FPDFPageObj_CreateTextObj(context.document, part.font, size);
|
|
270
|
+
if (object) {
|
|
271
|
+
setText(pdfium, object, part.text);
|
|
272
|
+
lib.FPDFPageObj_SetFillColor(object, r, g, bl, 255);
|
|
273
|
+
lib.FPDFPageObj_Transform(object, a, b, c, d, e + cursor * a, f + cursor * b);
|
|
274
|
+
}
|
|
275
|
+
objects.push(object);
|
|
276
|
+
cursor += measurer.advance(part.font, size, part.text);
|
|
277
|
+
}
|
|
278
|
+
// Every part must read back as written, or the original font cannot
|
|
279
|
+
// encode its share and the whole object goes to the fallback instead.
|
|
280
|
+
if (objects.some((object) => object === 0)) {
|
|
281
|
+
for (const object of objects)
|
|
282
|
+
if (object)
|
|
283
|
+
lib.FPDFPageObj_Destroy(object);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
lib.FPDFPage_RemoveObject(page, old);
|
|
287
|
+
objects.forEach((object, at) => lib.FPDFPage_InsertObjectAtIndex(page, object, index + at));
|
|
288
|
+
const textPage = lib.FPDFText_LoadPage(page);
|
|
289
|
+
let faithful = true;
|
|
290
|
+
try {
|
|
291
|
+
objects.forEach((object, at) => {
|
|
292
|
+
const read = pdfium.readWideString((buffer, bytes) => lib.FPDFTextObj_GetText(object, textPage, buffer, bytes));
|
|
293
|
+
// PDFium appends a generated space to an object a gap follows.
|
|
294
|
+
if (read.trimEnd() !== parts[at].text.trimEnd())
|
|
295
|
+
faithful = false;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
lib.FPDFText_ClosePage(textPage);
|
|
300
|
+
}
|
|
301
|
+
if (!faithful) {
|
|
302
|
+
for (const object of objects) {
|
|
303
|
+
lib.FPDFPage_RemoveObject(page, object);
|
|
304
|
+
lib.FPDFPageObj_Destroy(object);
|
|
305
|
+
}
|
|
306
|
+
lib.FPDFPage_InsertObjectAtIndex(page, old, index);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
lib.FPDFPageObj_Destroy(old);
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
if (!written)
|
|
313
|
+
return undefined;
|
|
314
|
+
const partCount = [before, middle, after].filter(Boolean).length;
|
|
315
|
+
const records = Array.from({ length: partCount }, (_, at) => {
|
|
316
|
+
if (at === 0)
|
|
317
|
+
return { id: location.record.id, type: OBJECT_TEXT };
|
|
318
|
+
const id = context.newId(location.pageIndex);
|
|
319
|
+
createdIds.push(id);
|
|
320
|
+
return { id, type: OBJECT_TEXT };
|
|
321
|
+
});
|
|
322
|
+
context.spliceObjects(location.pageIndex, index, 1, records);
|
|
323
|
+
return {
|
|
324
|
+
createdIds,
|
|
325
|
+
changedPages: [location.pageIndex],
|
|
326
|
+
warnings: [
|
|
327
|
+
{
|
|
328
|
+
code: "font-substitution",
|
|
329
|
+
message: fallback.substitution ??
|
|
330
|
+
`${style.fontFamily} cannot draw the new text; ${fallback.family} is used for it`,
|
|
331
|
+
details: { elementId: location.record.id },
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/** Replaces the object with one in a covering font at the same place, size and colour. */
|
|
337
|
+
function replaceWithFallback(context, target, text) {
|
|
338
|
+
const { pdfium } = context;
|
|
339
|
+
const { lib } = pdfium;
|
|
340
|
+
const { location, element } = target;
|
|
341
|
+
const style = element.textStyle;
|
|
342
|
+
const index = location.indexes[0];
|
|
343
|
+
const font = context.fonts.resolve(pdfium, context.document, {
|
|
344
|
+
family: style.fontFamily,
|
|
345
|
+
bold: style.bold,
|
|
346
|
+
italic: style.italic,
|
|
347
|
+
text,
|
|
348
|
+
});
|
|
349
|
+
context.withPage(location.pageIndex, (page) => {
|
|
350
|
+
const old = lib.FPDFPage_GetObject(page, index);
|
|
351
|
+
const matrix = pdfium.readNumbers(6, "float", ([pointer]) => lib.FPDFPageObj_GetMatrix(old, pointer)) ?? [1, 0, 0, 1, 0, 0];
|
|
352
|
+
const size = pdfium.readNumbers(1, "float", ([pointer]) => lib.FPDFTextObj_GetFontSize(old, pointer))?.[0] ?? style.fontSize;
|
|
353
|
+
const object = lib.FPDFPageObj_CreateTextObj(context.document, font.handle, size);
|
|
354
|
+
setText(pdfium, object, text);
|
|
355
|
+
const [r, g, b] = parseColor(style.color);
|
|
356
|
+
lib.FPDFPageObj_SetFillColor(object, r, g, b, 255);
|
|
357
|
+
const [a, bb, c, d, e, f] = matrix;
|
|
358
|
+
lib.FPDFPageObj_Transform(object, a, bb, c, d, e, f);
|
|
359
|
+
lib.FPDFPage_RemoveObject(page, old);
|
|
360
|
+
lib.FPDFPageObj_Destroy(old);
|
|
361
|
+
lib.FPDFPage_InsertObjectAtIndex(page, object, index);
|
|
362
|
+
});
|
|
363
|
+
context.spliceObjects(location.pageIndex, index, 1, [
|
|
364
|
+
{ id: location.record.id, type: OBJECT_TEXT },
|
|
365
|
+
]);
|
|
366
|
+
return {
|
|
367
|
+
createdIds: [],
|
|
368
|
+
changedPages: [location.pageIndex],
|
|
369
|
+
warnings: [
|
|
370
|
+
{
|
|
371
|
+
code: "font-substitution",
|
|
372
|
+
message: font.substitution ??
|
|
373
|
+
`${style.fontFamily} cannot draw the new text; ${font.family} is used`,
|
|
374
|
+
details: { elementId: location.record.id },
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Gives the object a new font size by rebuilding it with the same font, text,
|
|
381
|
+
* colour and matrix, then moving it so its top-left corner stays put.
|
|
382
|
+
*/
|
|
383
|
+
function resize(context, target, fontSize) {
|
|
384
|
+
const { pdfium } = context;
|
|
385
|
+
const { lib } = pdfium;
|
|
386
|
+
const { location } = target;
|
|
387
|
+
const index = location.indexes[0];
|
|
388
|
+
const geometry = context.geometry(location.pageIndex);
|
|
389
|
+
const before = target.element.bounds;
|
|
390
|
+
context.withPage(location.pageIndex, (page) => {
|
|
391
|
+
const old = lib.FPDFPage_GetObject(page, index);
|
|
392
|
+
const textPage = lib.FPDFText_LoadPage(page);
|
|
393
|
+
let text;
|
|
394
|
+
try {
|
|
395
|
+
text = pdfium.readWideString((buffer, bytes) => lib.FPDFTextObj_GetText(old, textPage, buffer, bytes));
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
lib.FPDFText_ClosePage(textPage);
|
|
399
|
+
}
|
|
400
|
+
const matrix = pdfium.readNumbers(6, "float", ([pointer]) => lib.FPDFPageObj_GetMatrix(old, pointer)) ?? [1, 0, 0, 1, 0, 0];
|
|
401
|
+
const color = pdfium.readNumbers(4, "i32", ([r, g, b, a]) => lib.FPDFPageObj_GetFillColor(old, r, g, b, a)) ?? [0, 0, 0, 255];
|
|
402
|
+
const object = lib.FPDFPageObj_CreateTextObj(context.document, lib.FPDFTextObj_GetFont(old), fontSize);
|
|
403
|
+
setText(pdfium, object, text);
|
|
404
|
+
lib.FPDFPageObj_SetFillColor(object, color[0], color[1], color[2], color[3]);
|
|
405
|
+
const [a, b, c, d, e, f] = matrix;
|
|
406
|
+
lib.FPDFPageObj_Transform(object, a, b, c, d, e, f);
|
|
407
|
+
lib.FPDFPage_RemoveObject(page, old);
|
|
408
|
+
lib.FPDFPageObj_Destroy(old);
|
|
409
|
+
lib.FPDFPage_InsertObjectAtIndex(page, object, index);
|
|
410
|
+
// Anchor the top-left corner where it was.
|
|
411
|
+
const after = objectBounds(pdfium, object, geometry);
|
|
412
|
+
if (after) {
|
|
413
|
+
const from = pageToUser(geometry, 0, 0);
|
|
414
|
+
const to = pageToUser(geometry, before.x - after.x, before.y - after.y);
|
|
415
|
+
lib.FPDFPageObj_Transform(object, 1, 0, 0, 1, to.x - from.x, to.y - from.y);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
context.spliceObjects(location.pageIndex, index, 1, [
|
|
419
|
+
{ id: location.record.id, type: OBJECT_TEXT },
|
|
420
|
+
]);
|
|
421
|
+
}
|
|
422
|
+
function result(location) {
|
|
423
|
+
return { createdIds: [], changedPages: [location.pageIndex], warnings: [] };
|
|
424
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { EditWorkerFont } from "../../../worker-protocol.js";
|
|
2
|
+
import type { Pdfium } from "./pdfium.js";
|
|
3
|
+
export declare const FALLBACK_FAMILY = "Noto Sans";
|
|
4
|
+
export declare function isStandardFamily(family: string): boolean;
|
|
5
|
+
/** The standard-14 name for a family and style, e.g. Helvetica + bold. */
|
|
6
|
+
export declare function standardFontName(family: string, bold: boolean, italic: boolean): string | undefined;
|
|
7
|
+
/** The first character a standard font cannot encode, if any. */
|
|
8
|
+
export declare function firstNonWinAnsi(text: string): string | undefined;
|
|
9
|
+
export interface FontRequest {
|
|
10
|
+
readonly family: string;
|
|
11
|
+
readonly bold: boolean;
|
|
12
|
+
readonly italic: boolean;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedFont {
|
|
16
|
+
readonly handle: number;
|
|
17
|
+
/** Family actually used. */
|
|
18
|
+
readonly family: string;
|
|
19
|
+
/** Set when the font is not the one asked for. */
|
|
20
|
+
readonly substitution?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Why a request cannot be satisfied. */
|
|
23
|
+
export interface FontProblem {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly code: "unknown-font" | "font-unavailable";
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Host-registered fonts and the fallback face, fetched on demand and loaded
|
|
30
|
+
* into whichever PDFium document needs them.
|
|
31
|
+
*/
|
|
32
|
+
export declare class FontLibrary {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(fetchBytes: (url: string) => Promise<Uint8Array>);
|
|
35
|
+
setFallbackUrl(url: string | undefined): void;
|
|
36
|
+
register(fonts: readonly EditWorkerFont[]): void;
|
|
37
|
+
/** Whether `family` names a registered font. */
|
|
38
|
+
hasFamily(family: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Fetches whatever the given texts and families may need, so that
|
|
41
|
+
* validation and drawing can run synchronously afterwards.
|
|
42
|
+
*/
|
|
43
|
+
prepare(requests: readonly {
|
|
44
|
+
family: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}[]): Promise<void>;
|
|
47
|
+
/** Checks a request against what is loaded; `undefined` means it will resolve. */
|
|
48
|
+
problem(request: FontRequest): FontProblem | undefined;
|
|
49
|
+
/** The font to draw `request` with in `document`; `prepare` must have run. */
|
|
50
|
+
resolve(pdfium: Pdfium, document: number, request: FontRequest): ResolvedFont;
|
|
51
|
+
/** Closes the fonts loaded into a document that is going away. */
|
|
52
|
+
release(pdfium: Pdfium, document: number): void;
|
|
53
|
+
covers(bytes: Uint8Array, text: string): boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface CmapCoverage {
|
|
56
|
+
has(codePoint: number): boolean;
|
|
57
|
+
/** Mapped to a glyph with outline data; false for an emptied glyph of a subset font. */
|
|
58
|
+
drawable(codePoint: number): boolean;
|
|
59
|
+
/** The glyph id a code point maps to, when the font's cmap was parsed. */
|
|
60
|
+
glyph(codePoint: number): number | undefined;
|
|
61
|
+
}
|
|
62
|
+
export declare function parseCmap(bytes: Uint8Array): CmapCoverage;
|
|
63
|
+
/**
|
|
64
|
+
* Measures text with PDFium's own metrics by building throw-away text
|
|
65
|
+
* objects, so layout agrees with what PDFium draws. Advances are cached per
|
|
66
|
+
* character and font at 1000 pt and scale linearly.
|
|
67
|
+
*/
|
|
68
|
+
export declare class TextMeasurer {
|
|
69
|
+
#private;
|
|
70
|
+
constructor(pdfium: Pdfium, document: number);
|
|
71
|
+
/** Width of `text` in points at `fontSize`, without kerning. */
|
|
72
|
+
advance(font: number, fontSize: number, text: string): number;
|
|
73
|
+
/** Ascent above and descent below the baseline, in points, both positive. */
|
|
74
|
+
metrics(font: number, fontSize: number): {
|
|
75
|
+
ascent: number;
|
|
76
|
+
descent: number;
|
|
77
|
+
};
|
|
78
|
+
}
|