web-doc 0.11.0 → 0.12.1
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/dist/adapters/pdf.js +19 -8
- package/dist/edit/engine.d.ts +2 -1
- package/dist/edit/pdf/engine/document.d.ts +4 -2
- package/dist/edit/pdf/engine/document.js +115 -12
- package/dist/edit/pdf/engine/elements.d.ts +5 -1
- package/dist/edit/pdf/engine/elements.js +88 -6
- package/dist/edit/pdf/engine/existing-text.js +33 -64
- package/dist/edit/pdf/engine/fonts.d.ts +14 -0
- package/dist/edit/pdf/engine/fonts.js +114 -0
- package/dist/edit/pdf/engine/handler.js +2 -0
- package/dist/edit/pdf/engine/layout.js +1 -1
- package/dist/edit/pdf/engine/operations.d.ts +7 -1
- package/dist/edit/pdf/engine/paragraph-edit.d.ts +4 -0
- package/dist/edit/pdf/engine/paragraph-edit.js +331 -0
- package/dist/edit/pdf/engine/paragraph.d.ts +31 -0
- package/dist/edit/pdf/engine/paragraph.js +445 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +4 -0
- package/dist/edit/pdf/engine/text-box.js +16 -3
- package/dist/edit/pdf/engine/text-font.js +2 -19
- package/dist/edit/pdf/engine/transform.js +14 -3
- package/dist/edit/pdf/provider.d.ts +3 -1
- package/dist/edit/pdf/provider.js +3 -0
- package/dist/edit/pdf/range-map.d.ts +2 -1
- package/dist/edit/pdf/range-map.js +38 -8
- package/dist/edit/pdf/schemas.d.ts +2 -0
- package/dist/edit/pdf/schemas.js +37 -0
- package/dist/edit/pdf/session.d.ts +3 -1
- package/dist/edit/pdf/session.js +35 -6
- package/dist/edit/pdf/types.d.ts +38 -8
- package/dist/edit/session.js +5 -0
- package/dist/edit/types.d.ts +13 -0
- package/dist/viewport.js +11 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/dist/workers/pdf-edit-worker.js +2026 -1004
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { ViewerError } from "../../../errors.js";
|
|
2
|
+
import { MARK_NAME, MARK_PARAM, OBJECT_PATH, OBJECT_TEXT } from "./elements.js";
|
|
3
|
+
import { fontCanDraw, isStandardFamily, validateScript } from "./fonts.js";
|
|
4
|
+
import { pageToUser, round } from "./geometry.js";
|
|
5
|
+
import { layoutText } from "./text-layout.js";
|
|
6
|
+
import { parseColor, placeUpright, setText } from "./text-box.js";
|
|
7
|
+
import { intersects, } from "./paragraph.js";
|
|
8
|
+
export const replaceParagraphText = {
|
|
9
|
+
validate(operation, context, issue) {
|
|
10
|
+
const target = targetOf(operation.target, context, issue);
|
|
11
|
+
if (!target)
|
|
12
|
+
return;
|
|
13
|
+
const text = replacement(operation, target.spec.text, issue);
|
|
14
|
+
if (text === undefined || !validateScript(text, issue))
|
|
15
|
+
return;
|
|
16
|
+
if (text.length > 20000) {
|
|
17
|
+
issue("/text", "max-length", "The complete paragraph cannot exceed 20000 characters");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
prepare(context, target, { ...target.spec, text }, issue);
|
|
21
|
+
},
|
|
22
|
+
apply(operation, context) {
|
|
23
|
+
const target = requireTarget(operation.target, context);
|
|
24
|
+
const text = replacement(operation, target.spec.text);
|
|
25
|
+
if (text === undefined)
|
|
26
|
+
throw new ViewerError("invalid-operation", "Invalid paragraph range");
|
|
27
|
+
return rebuild(context, target, { ...target.spec, text });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const paragraphSetTextStyle = {
|
|
31
|
+
validate(operation, context, issue) {
|
|
32
|
+
const target = targetOf(operation.target, context, issue);
|
|
33
|
+
if (!target)
|
|
34
|
+
return;
|
|
35
|
+
for (const field of [
|
|
36
|
+
"fontFamily",
|
|
37
|
+
"bold",
|
|
38
|
+
"italic",
|
|
39
|
+
"align",
|
|
40
|
+
"lineHeight",
|
|
41
|
+
])
|
|
42
|
+
if (operation.style[field] !== undefined)
|
|
43
|
+
issue(`/style/${field}`, "unsupported-style", `Only color and fontSize can change on imported paragraphs; ${field} cannot`);
|
|
44
|
+
const spec = styled(target.spec, operation);
|
|
45
|
+
if (spec.style.fontSize !== target.spec.style.fontSize)
|
|
46
|
+
prepare(context, target, spec, issue);
|
|
47
|
+
},
|
|
48
|
+
apply(operation, context) {
|
|
49
|
+
const target = requireTarget(operation.target, context);
|
|
50
|
+
const spec = styled(target.spec, operation);
|
|
51
|
+
if (spec.style.fontSize === target.spec.style.fontSize)
|
|
52
|
+
return colorInPlace(context, target, spec);
|
|
53
|
+
return rebuild(context, target, spec);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
/** Color never rewrites glyphs or changes an imported font resource. */
|
|
57
|
+
function colorInPlace(context, target, spec) {
|
|
58
|
+
const { pdfium } = context;
|
|
59
|
+
const { lib } = pdfium;
|
|
60
|
+
const color = parseColor(spec.style.color);
|
|
61
|
+
const persisted = target.paragraph.memberIds.includes(spec.id);
|
|
62
|
+
context.withPage(target.paragraph.pageIndex, (page) => {
|
|
63
|
+
for (const index of target.indexes) {
|
|
64
|
+
const object = lib.FPDFPage_GetObject(page, index);
|
|
65
|
+
if (lib.FPDFPageObj_GetType(object) === OBJECT_TEXT)
|
|
66
|
+
lib.FPDFPageObj_SetFillColor(object, ...color, 255);
|
|
67
|
+
if (!persisted)
|
|
68
|
+
continue;
|
|
69
|
+
for (let at = 0; at < lib.FPDFPageObj_CountMarks(object); at += 1) {
|
|
70
|
+
const mark = lib.FPDFPageObj_GetMark(object, at);
|
|
71
|
+
const name = pdfium.readWideStringOut((buffer, bytes, out) => lib.FPDFPageObjMark_GetName(mark, buffer, bytes, out));
|
|
72
|
+
if (name === MARK_NAME)
|
|
73
|
+
lib.FPDFPageObjMark_SetStringParam(context.document, object, mark, MARK_PARAM, JSON.stringify(spec));
|
|
74
|
+
}
|
|
75
|
+
context.spliceObjects(target.paragraph.pageIndex, index, 1, [
|
|
76
|
+
{ id: spec.id, type: lib.FPDFPageObj_GetType(object), mark: spec },
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
createdIds: [],
|
|
82
|
+
changedPages: [target.paragraph.pageIndex],
|
|
83
|
+
warnings: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function styled(spec, operation) {
|
|
87
|
+
return {
|
|
88
|
+
...spec,
|
|
89
|
+
style: {
|
|
90
|
+
...spec.style,
|
|
91
|
+
...(operation.style.fontSize === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { fontSize: round(operation.style.fontSize) }),
|
|
94
|
+
...(operation.style.color === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { color: operation.style.color }),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function targetOf(id, context, issue) {
|
|
101
|
+
const target = context.paragraph(id);
|
|
102
|
+
if (!target || target.paragraph.id !== id) {
|
|
103
|
+
issue("/target", "unsupported-target", "The target must be a resolved native paragraph id");
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
function requireTarget(id, context) {
|
|
109
|
+
const target = targetOf(id, context, () => { });
|
|
110
|
+
if (!target)
|
|
111
|
+
throw new ViewerError("invalid-operation", "The paragraph no longer exists");
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
function replacement(operation, previous, issue = () => { }) {
|
|
115
|
+
const range = operation.range;
|
|
116
|
+
if (!range)
|
|
117
|
+
return operation.text;
|
|
118
|
+
const valid = [range.start, range.end].every((position) => position.elementId === operation.target &&
|
|
119
|
+
Number.isInteger(position.offset) &&
|
|
120
|
+
position.offset >= 0 &&
|
|
121
|
+
position.offset <= previous.length);
|
|
122
|
+
if (!valid || range.start.offset > range.end.offset) {
|
|
123
|
+
issue("/range", "invalid-range", "The range must lie inside the paragraph's logical text");
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return (previous.slice(0, range.start.offset) +
|
|
127
|
+
operation.text +
|
|
128
|
+
previous.slice(range.end.offset));
|
|
129
|
+
}
|
|
130
|
+
function fontFor(context, target, spec, page) {
|
|
131
|
+
const { lib } = context.pdfium;
|
|
132
|
+
const original = target.indexes
|
|
133
|
+
.map((index) => lib.FPDFPage_GetObject(page, index))
|
|
134
|
+
.find((object) => lib.FPDFPageObj_GetType(object) === OBJECT_TEXT);
|
|
135
|
+
const native = original === undefined ? undefined : lib.FPDFTextObj_GetFont(original);
|
|
136
|
+
if (native !== undefined && fontCanDraw(context.pdfium, native, spec.text))
|
|
137
|
+
return {
|
|
138
|
+
handle: native,
|
|
139
|
+
family: spec.style.fontFamily,
|
|
140
|
+
substituted: false,
|
|
141
|
+
};
|
|
142
|
+
const family = isStandardFamily(spec.style.fontFamily) ||
|
|
143
|
+
context.fonts.hasFamily(spec.style.fontFamily)
|
|
144
|
+
? spec.style.fontFamily
|
|
145
|
+
: "Helvetica";
|
|
146
|
+
const request = {
|
|
147
|
+
family,
|
|
148
|
+
bold: spec.style.bold,
|
|
149
|
+
italic: spec.style.italic,
|
|
150
|
+
text: spec.text,
|
|
151
|
+
};
|
|
152
|
+
const problem = context.fonts.problem(request);
|
|
153
|
+
if (problem)
|
|
154
|
+
return { problem };
|
|
155
|
+
const font = context.fonts.resolve(context.pdfium, context.document, request);
|
|
156
|
+
return {
|
|
157
|
+
handle: font.handle,
|
|
158
|
+
family: font.family,
|
|
159
|
+
substituted: original !== undefined || !isStandardFamily(spec.style.fontFamily),
|
|
160
|
+
...(font.substitution ? { substitution: font.substitution } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Validate the actual final baseline/descent, not line-count × leading. */
|
|
164
|
+
function prepare(context, target, spec, issue) {
|
|
165
|
+
return context.readPage(target.paragraph.pageIndex, (page) => {
|
|
166
|
+
const font = fontFor(context, target, spec, page);
|
|
167
|
+
if ("problem" in font) {
|
|
168
|
+
issue(font.problem.path, font.problem.code, font.problem.message);
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const metrics = context.measurer.metrics(font.handle, spec.style.fontSize);
|
|
172
|
+
const layout = layoutText({
|
|
173
|
+
text: spec.text,
|
|
174
|
+
width: spec.rect.width,
|
|
175
|
+
height: Number.POSITIVE_INFINITY,
|
|
176
|
+
fontSize: spec.style.fontSize,
|
|
177
|
+
lineHeight: spec.style.lineHeight,
|
|
178
|
+
align: "left",
|
|
179
|
+
ascent: spec.baselineOffset * spec.style.fontSize,
|
|
180
|
+
advance: (text) => context.measurer.advance(font.handle, spec.style.fontSize, text),
|
|
181
|
+
});
|
|
182
|
+
const last = layout.lines.at(-1);
|
|
183
|
+
const height = spec.text.trim() && last
|
|
184
|
+
? last.baseline + metrics.descent
|
|
185
|
+
: spec.rect.height;
|
|
186
|
+
const rect = { ...spec.rect, height: Math.max(height, 0.001) };
|
|
187
|
+
const size = context.pageSize(target.paragraph.pageIndex);
|
|
188
|
+
const members = new Set(target.paragraph.memberIds);
|
|
189
|
+
const collision = context
|
|
190
|
+
.pageElements(target.paragraph.pageIndex)
|
|
191
|
+
.some((element) => {
|
|
192
|
+
if (members.has(element.id) || !intersects(rect, element.bounds))
|
|
193
|
+
return false;
|
|
194
|
+
// An already-present background stays in its own drawing position.
|
|
195
|
+
const old = target.spec.rect;
|
|
196
|
+
const b = element.bounds;
|
|
197
|
+
const location = context.locate(element.id);
|
|
198
|
+
const behind = location &&
|
|
199
|
+
location.indexes.every((index) => index < target.indexes[0]);
|
|
200
|
+
return !(element.text === undefined &&
|
|
201
|
+
behind &&
|
|
202
|
+
b.x <= old.x &&
|
|
203
|
+
b.y <= old.y &&
|
|
204
|
+
b.x + b.width >= rect.x + rect.width &&
|
|
205
|
+
b.y + b.height >= rect.y + rect.height);
|
|
206
|
+
});
|
|
207
|
+
if (collision ||
|
|
208
|
+
rect.x < 0 ||
|
|
209
|
+
rect.y < 0 ||
|
|
210
|
+
rect.x + rect.width > size.width + 0.01 ||
|
|
211
|
+
rect.y + rect.height > size.height + 0.01) {
|
|
212
|
+
issue("/text", "paragraph-overflow", "The paragraph would overlap neighboring content or leave the page");
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const warnings = font.substituted
|
|
216
|
+
? [
|
|
217
|
+
{
|
|
218
|
+
code: "font-substitution",
|
|
219
|
+
message: "substitution" in font && font.substitution
|
|
220
|
+
? font.substitution
|
|
221
|
+
: `${spec.style.fontFamily} cannot draw the new paragraph; ${font.family} is used`,
|
|
222
|
+
details: { elementId: spec.id },
|
|
223
|
+
},
|
|
224
|
+
]
|
|
225
|
+
: [];
|
|
226
|
+
return {
|
|
227
|
+
spec: {
|
|
228
|
+
...spec,
|
|
229
|
+
rect,
|
|
230
|
+
lines: layout.lines.map((line) => line.text).filter(Boolean),
|
|
231
|
+
style: {
|
|
232
|
+
...spec.style,
|
|
233
|
+
fontFamily: font.substituted ? font.family : spec.style.fontFamily,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
layout,
|
|
237
|
+
warnings,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function rebuild(context, target, spec) {
|
|
242
|
+
const issues = [];
|
|
243
|
+
const prepared = prepare(context, target, spec, (path, code, message) => issues.push({ path, code, message }));
|
|
244
|
+
if (!prepared)
|
|
245
|
+
throw new ViewerError("invalid-operation", "The paragraph cannot be changed", { details: { issues } });
|
|
246
|
+
const { pdfium } = context;
|
|
247
|
+
const { lib } = pdfium;
|
|
248
|
+
const pageIndex = target.paragraph.pageIndex;
|
|
249
|
+
const geometry = context.geometry(pageIndex);
|
|
250
|
+
const first = target.indexes[0];
|
|
251
|
+
context.withPage(pageIndex, (page) => {
|
|
252
|
+
const font = fontFor(context, target, spec, page);
|
|
253
|
+
if ("problem" in font)
|
|
254
|
+
throw new ViewerError("invalid-operation", font.problem.message);
|
|
255
|
+
const objects = [];
|
|
256
|
+
const [r, g, b] = parseColor(spec.style.color);
|
|
257
|
+
// Construct before removing borrowed font owners. Empty drafts use a
|
|
258
|
+
// non-painting native path, so no placeholder character leaks into text.
|
|
259
|
+
if (!spec.text.trim()) {
|
|
260
|
+
const rect = prepared.spec.rect;
|
|
261
|
+
const origin = pageToUser(geometry, rect.x, rect.y + rect.height);
|
|
262
|
+
const object = lib.FPDFPageObj_CreateNewRect(origin.x, origin.y, rect.width, rect.height);
|
|
263
|
+
// PDFium discards a path ending in `n` when reopening. A zero-alpha
|
|
264
|
+
// fill preserves the native marked object without painting any pixels.
|
|
265
|
+
lib.FPDFPageObj_SetFillColor(object, 0, 0, 0, 0);
|
|
266
|
+
lib.FPDFPath_SetDrawMode(object, 1, false);
|
|
267
|
+
objects.push({ object, type: OBJECT_PATH });
|
|
268
|
+
}
|
|
269
|
+
else
|
|
270
|
+
for (const line of prepared.layout.lines) {
|
|
271
|
+
if (!line.text)
|
|
272
|
+
continue;
|
|
273
|
+
const object = lib.FPDFPageObj_CreateTextObj(context.document, font.handle, spec.style.fontSize);
|
|
274
|
+
if (!object) {
|
|
275
|
+
for (const made of objects)
|
|
276
|
+
lib.FPDFPageObj_Destroy(made.object);
|
|
277
|
+
throw new ViewerError("edit-failed", "PDFium could not create paragraph text");
|
|
278
|
+
}
|
|
279
|
+
setText(pdfium, object, line.text);
|
|
280
|
+
lib.FPDFPageObj_SetFillColor(object, r, g, b, 255);
|
|
281
|
+
placeUpright(pdfium, object, geometry, spec.rect.x + line.x, spec.rect.y + line.baseline);
|
|
282
|
+
objects.push({ object, type: OBJECT_TEXT });
|
|
283
|
+
}
|
|
284
|
+
const params = JSON.stringify(prepared.spec);
|
|
285
|
+
for (const { object } of objects) {
|
|
286
|
+
const mark = lib.FPDFPageObj_AddMark(object, MARK_NAME);
|
|
287
|
+
lib.FPDFPageObjMark_SetStringParam(context.document, object, mark, MARK_PARAM, params);
|
|
288
|
+
}
|
|
289
|
+
for (const index of [...target.indexes].reverse()) {
|
|
290
|
+
const object = lib.FPDFPage_GetObject(page, index);
|
|
291
|
+
lib.FPDFPage_RemoveObject(page, object);
|
|
292
|
+
lib.FPDFPageObj_Destroy(object);
|
|
293
|
+
context.spliceObjects(pageIndex, index, 1, []);
|
|
294
|
+
}
|
|
295
|
+
objects.forEach(({ object }, at) => lib.FPDFPage_InsertObjectAtIndex(page, object, first + at));
|
|
296
|
+
const textPage = lib.FPDFText_LoadPage(page);
|
|
297
|
+
try {
|
|
298
|
+
const drawn = objects
|
|
299
|
+
.filter(({ type }) => type === OBJECT_TEXT)
|
|
300
|
+
.map(({ object }) => pdfium
|
|
301
|
+
.readWideString((buffer, bytes) => lib.FPDFTextObj_GetText(object, textPage, buffer, bytes))
|
|
302
|
+
.trimEnd());
|
|
303
|
+
if (drawn.length !== prepared.spec.lines.length ||
|
|
304
|
+
drawn.some((line, index) => line !== prepared.spec.lines[index]))
|
|
305
|
+
throw new ViewerError("edit-failed", "The native font could not encode the paragraph faithfully");
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
lib.FPDFText_ClosePage(textPage);
|
|
309
|
+
}
|
|
310
|
+
context.spliceObjects(pageIndex, first, 0, objects.map(({ type }) => ({ id: spec.id, type, mark: prepared.spec })));
|
|
311
|
+
});
|
|
312
|
+
const promoted = !target.paragraph.memberIds.includes(spec.id);
|
|
313
|
+
// The logical paragraph already existed before promotion. Reporting it as
|
|
314
|
+
// newly created would incorrectly destroy its ranges when Undo restores rows.
|
|
315
|
+
return {
|
|
316
|
+
createdIds: [],
|
|
317
|
+
removedIds: promoted ? target.paragraph.memberIds : [],
|
|
318
|
+
...(promoted
|
|
319
|
+
? {
|
|
320
|
+
textAnchorMigrations: target.paragraph.members.map((member) => ({
|
|
321
|
+
sourceElementId: member.elementId,
|
|
322
|
+
targetElementId: spec.id,
|
|
323
|
+
sourceLength: member.end - member.start,
|
|
324
|
+
targetOffset: member.start,
|
|
325
|
+
})),
|
|
326
|
+
}
|
|
327
|
+
: {}),
|
|
328
|
+
changedPages: [pageIndex],
|
|
329
|
+
warnings: prepared.warnings,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PageRect } from "../../types.js";
|
|
2
|
+
import type { PdfElement, PdfTextParagraph } from "../types.js";
|
|
3
|
+
import { type MarkParams, type ObjectRecord } from "./elements.js";
|
|
4
|
+
import { type PageGeometry } from "./geometry.js";
|
|
5
|
+
import { type TextMeasurer } from "./fonts.js";
|
|
6
|
+
import type { Pdfium } from "./pdfium.js";
|
|
7
|
+
/** Persisted inputs, validated at the PDF marked-content boundary. */
|
|
8
|
+
export interface ParagraphSpec extends MarkParams {
|
|
9
|
+
readonly kind: "paragraph";
|
|
10
|
+
readonly rect: PageRect;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
/** Actual native line strings, retained to detect edits made by another tool. */
|
|
13
|
+
readonly lines: readonly string[];
|
|
14
|
+
readonly style: PdfTextParagraph["textStyle"];
|
|
15
|
+
/** First baseline relative to rect.y, in multiples of the current style font size. */
|
|
16
|
+
readonly baselineOffset: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ParagraphTarget {
|
|
19
|
+
readonly paragraph: PdfTextParagraph;
|
|
20
|
+
readonly spec: ParagraphSpec;
|
|
21
|
+
/** Native drawing-order indexes, not logical reading order. */
|
|
22
|
+
readonly indexes: readonly number[];
|
|
23
|
+
}
|
|
24
|
+
export declare function paragraphElement(paragraph: PdfTextParagraph): PdfElement;
|
|
25
|
+
/**
|
|
26
|
+
* Conservative import recognition. Only whole, equally styled native lines
|
|
27
|
+
* with uniform upright matrices and regular left-aligned leading participate.
|
|
28
|
+
* Content-stream order is deliberately irrelevant to reading order.
|
|
29
|
+
*/
|
|
30
|
+
export declare function discoverParagraphs(pdfium: Pdfium, page: number, pageIndex: number, geometry: PageGeometry, records: readonly ObjectRecord[], elements: readonly PdfElement[], textPage: number, measurer: TextMeasurer): ReadonlyMap<string, ParagraphTarget>;
|
|
31
|
+
export declare function intersects(a: PageRect, b: PageRect): boolean;
|