web-doc 0.10.0 → 0.12.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/dist/adapters/office.js +4 -2
- package/dist/adapters/pdf.js +19 -8
- package/dist/edit/docx/engine.d.ts +9 -1
- package/dist/edit/docx/engine.js +22 -0
- package/dist/edit/docx/provider.d.ts +3 -1
- package/dist/edit/docx/provider.js +3 -0
- package/dist/edit/docx/session.d.ts +6 -2
- package/dist/edit/docx/session.js +7 -1
- package/dist/edit/docx/style.d.ts +1 -1
- package/dist/edit/docx/style.js +3 -2
- package/dist/edit/docx/types.d.ts +11 -1
- 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 +16 -61
- 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 +3 -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/pptx/elements.d.ts +4 -1
- package/dist/edit/pptx/elements.js +35 -0
- package/dist/edit/pptx/engine.d.ts +5 -1
- package/dist/edit/pptx/engine.js +11 -0
- package/dist/edit/pptx/handler.js +8 -0
- package/dist/edit/pptx/provider.d.ts +3 -1
- package/dist/edit/pptx/provider.js +3 -0
- package/dist/edit/pptx/session.d.ts +6 -2
- package/dist/edit/pptx/session.js +7 -1
- package/dist/edit/pptx/types.d.ts +11 -1
- package/dist/edit/range-style.d.ts +24 -0
- package/dist/edit/range-style.js +46 -0
- 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/ooxml-edit-worker.js +90 -2
- package/dist/workers/pdf-edit-worker.js +2019 -1003
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateSchema } from "../../schema.js";
|
|
2
|
-
import { tableMarkSchema, tableMemberMarkSchema, textBoxMarkSchema, } from "../schemas.js";
|
|
3
|
-
import { round, roundRect, unionRects, userRectToPage, } from "./geometry.js";
|
|
2
|
+
import { tableMarkSchema, tableMemberMarkSchema, textBoxMarkSchema, paragraphMarkSchema, } from "../schemas.js";
|
|
3
|
+
import { pageToUser, round, roundRect, unionRects, userRectToPage, } from "./geometry.js";
|
|
4
4
|
/** FPDFPageObj_GetType values. */
|
|
5
5
|
export const OBJECT_TEXT = 1;
|
|
6
6
|
export const OBJECT_PATH = 2;
|
|
@@ -11,6 +11,20 @@ export const MARK_PARAM = "webdoc";
|
|
|
11
11
|
/** Font descriptor flag bits (PDF 32000-1 table 123). */
|
|
12
12
|
const FLAG_ITALIC = 1 << 6;
|
|
13
13
|
const FLAG_FORCE_BOLD = 1 << 18;
|
|
14
|
+
export function paragraphSpecOf(mark) {
|
|
15
|
+
return validateSchema(mark, paragraphMarkSchema, 0).length === 0
|
|
16
|
+
? mark
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Paragraph marks store the upright frame; the page may subsequently rotate. */
|
|
20
|
+
export function paragraphBounds(spec, geometry) {
|
|
21
|
+
if (geometry.rotation === 0)
|
|
22
|
+
return spec.rect;
|
|
23
|
+
const upright = { ...geometry, rotation: 0 };
|
|
24
|
+
const top = pageToUser(upright, spec.rect.x, spec.rect.y);
|
|
25
|
+
const bottom = pageToUser(upright, spec.rect.x + spec.rect.width, spec.rect.y + spec.rect.height);
|
|
26
|
+
return roundRect(userRectToPage(geometry, top.x, bottom.y, bottom.x, top.y));
|
|
27
|
+
}
|
|
14
28
|
/**
|
|
15
29
|
* Reads the objects of a loaded page in drawing order and turns them into
|
|
16
30
|
* elements: plain objects become one element each, objects tagged with the
|
|
@@ -46,7 +60,7 @@ export function scanPage(pdfium, page, textPage, pageIndex, geometry, records) {
|
|
|
46
60
|
const members = groups.get(element.id);
|
|
47
61
|
const record = members && records.find((entry) => entry.id === element.id);
|
|
48
62
|
return members && record?.mark
|
|
49
|
-
? compositeElement(record.mark, members, pageIndex)
|
|
63
|
+
? compositeElement(record.mark, members, pageIndex, geometry)
|
|
50
64
|
: element;
|
|
51
65
|
}),
|
|
52
66
|
byObject,
|
|
@@ -77,6 +91,8 @@ function isValidMark(params) {
|
|
|
77
91
|
switch (params.kind) {
|
|
78
92
|
case "textBox":
|
|
79
93
|
return validateSchema(params, textBoxMarkSchema, 0).length === 0;
|
|
94
|
+
case "paragraph":
|
|
95
|
+
return validateSchema(params, paragraphMarkSchema, 0).length === 0;
|
|
80
96
|
case "table":
|
|
81
97
|
return (validateSchema(params, tableMarkSchema, 0).length === 0 ||
|
|
82
98
|
validateSchema(params, tableMemberMarkSchema, 0).length === 0);
|
|
@@ -104,11 +120,58 @@ export function markIsFresh(pdfium, page, textPage, geometry, mark, indexes) {
|
|
|
104
120
|
.map((object) => textOf(pdfium, object, textPage))
|
|
105
121
|
.join(" "));
|
|
106
122
|
const tolerance = 2;
|
|
107
|
-
if (mark.kind === "
|
|
123
|
+
if (mark.kind === "paragraph") {
|
|
124
|
+
const spec = paragraphSpecOf(mark);
|
|
125
|
+
if (!spec ||
|
|
126
|
+
objects.some((object) => {
|
|
127
|
+
if (lib.FPDFPageObj_GetType(object) !== OBJECT_TEXT) {
|
|
128
|
+
if (spec.text.trim().length !== 0 ||
|
|
129
|
+
lib.FPDFPageObj_GetType(object) !== OBJECT_PATH)
|
|
130
|
+
return true;
|
|
131
|
+
const mode = pdfium.readNumbers(2, "i32", ([fill, stroke]) => lib.FPDFPath_GetDrawMode(object, fill, stroke));
|
|
132
|
+
const color = pdfium.readNumbers(4, "i32", ([r, g, b, a]) => lib.FPDFPageObj_GetFillColor(object, r, g, b, a));
|
|
133
|
+
return mode?.[0] !== 1 || mode[1] !== 0 || color?.[3] !== 0;
|
|
134
|
+
}
|
|
135
|
+
const current = textStyle(pdfium, object);
|
|
136
|
+
return (current.fontSize !== round(spec.style.fontSize) ||
|
|
137
|
+
current.color !== spec.style.color ||
|
|
138
|
+
current.bold !== spec.style.bold ||
|
|
139
|
+
current.italic !== spec.style.italic);
|
|
140
|
+
}))
|
|
141
|
+
return false;
|
|
142
|
+
const frame = paragraphBounds(spec, geometry);
|
|
143
|
+
if (union.y + union.height > frame.y + frame.height + tolerance)
|
|
144
|
+
return false;
|
|
145
|
+
const nativeLines = objects
|
|
146
|
+
.filter((object) => lib.FPDFPageObj_GetType(object) === OBJECT_TEXT)
|
|
147
|
+
.map((object) => textOf(pdfium, object, textPage).trimEnd());
|
|
148
|
+
if (nativeLines.length !== spec.lines.length ||
|
|
149
|
+
nativeLines.some((line, index) => line !== spec.lines[index]))
|
|
150
|
+
return false;
|
|
151
|
+
// Native wrapping may split at whitespace or inside a long word, but it
|
|
152
|
+
// cannot invent, reorder or omit any non-whitespace logical character.
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
for (const line of nativeLines) {
|
|
155
|
+
while (!spec.text.startsWith(line, cursor) &&
|
|
156
|
+
cursor < spec.text.length &&
|
|
157
|
+
/\s/u.test(spec.text[cursor]))
|
|
158
|
+
cursor += 1;
|
|
159
|
+
if (!spec.text.startsWith(line, cursor))
|
|
160
|
+
return false;
|
|
161
|
+
cursor += line.length;
|
|
162
|
+
}
|
|
163
|
+
if (spec.text.slice(cursor).trim())
|
|
164
|
+
return false;
|
|
165
|
+
if (geometry.rotation !== 0)
|
|
166
|
+
return (union.x >= frame.x - tolerance &&
|
|
167
|
+
union.y >= frame.y - tolerance &&
|
|
168
|
+
union.x + union.width <= frame.x + frame.width + tolerance);
|
|
169
|
+
}
|
|
170
|
+
if (mark.kind === "textBox" || mark.kind === "paragraph") {
|
|
108
171
|
const rect = mark.rect;
|
|
109
172
|
const text = mark.text;
|
|
110
173
|
const style = mark.style;
|
|
111
|
-
return (drawn === normalizeText(text) &&
|
|
174
|
+
return ((mark.kind === "paragraph" || drawn === normalizeText(text)) &&
|
|
112
175
|
union.x >= rect.x - tolerance &&
|
|
113
176
|
union.x + union.width <= rect.x + rect.width + tolerance &&
|
|
114
177
|
// The first line's ink starts inside the box's first line band; the
|
|
@@ -169,7 +232,7 @@ function plainElement(pdfium, object, textPage, record, pageIndex, geometry) {
|
|
|
169
232
|
return { ...base, kind: "other", operations: TRANSFORM_OPERATIONS };
|
|
170
233
|
}
|
|
171
234
|
}
|
|
172
|
-
function compositeElement(mark, members, pageIndex) {
|
|
235
|
+
function compositeElement(mark, members, pageIndex, geometry) {
|
|
173
236
|
const bounds = roundRect(unionRects(members.map((member) => member.bounds)));
|
|
174
237
|
const text = members
|
|
175
238
|
.filter((member) => member.kind === "text" && member.text)
|
|
@@ -198,6 +261,25 @@ function compositeElement(mark, members, pageIndex) {
|
|
|
198
261
|
operations: TABLE_OPERATIONS,
|
|
199
262
|
};
|
|
200
263
|
}
|
|
264
|
+
const paragraph = paragraphSpecOf(mark);
|
|
265
|
+
if (paragraph)
|
|
266
|
+
return {
|
|
267
|
+
id: mark.id,
|
|
268
|
+
kind: "paragraph",
|
|
269
|
+
pageIndex,
|
|
270
|
+
bounds: paragraphBounds(paragraph, geometry),
|
|
271
|
+
text: paragraph.text,
|
|
272
|
+
textStyle: paragraph.style,
|
|
273
|
+
...(geometry.rotation ? { rotation: geometry.rotation * 90 } : {}),
|
|
274
|
+
operations: geometry.rotation
|
|
275
|
+
? ["deleteElement"]
|
|
276
|
+
: [
|
|
277
|
+
"replaceText",
|
|
278
|
+
"replaceParagraphText",
|
|
279
|
+
"setTextStyle",
|
|
280
|
+
"deleteElement",
|
|
281
|
+
],
|
|
282
|
+
};
|
|
201
283
|
const style = members.find((member) => member.textStyle)?.textStyle;
|
|
202
284
|
return {
|
|
203
285
|
id: mark.id,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { paragraphSetTextStyle, replaceParagraphText, } from "./paragraph-edit.js";
|
|
1
2
|
import { objectBounds, OBJECT_TEXT, textScale } from "./elements.js";
|
|
2
|
-
import {
|
|
3
|
+
import { fontCanRewrite, validateScript } from "./fonts.js";
|
|
3
4
|
import { pageToUser } from "./geometry.js";
|
|
4
5
|
import { parseColor, setText, textBoxReplaceText, textBoxSetTextStyle, textBoxTarget, } from "./text-box.js";
|
|
5
6
|
/*
|
|
@@ -10,6 +11,8 @@ import { parseColor, setText, textBoxReplaceText, textBoxSetTextStyle, textBoxTa
|
|
|
10
11
|
*/
|
|
11
12
|
export const replaceText = {
|
|
12
13
|
validate(operation, context, issue) {
|
|
14
|
+
if (context.paragraph(operation.target)?.paragraph.id === operation.target)
|
|
15
|
+
return replaceParagraphText.validate({ ...operation, op: "replaceParagraphText" }, context, issue);
|
|
13
16
|
const box = textBoxTarget(operation.target, context);
|
|
14
17
|
if (box) {
|
|
15
18
|
const spliced = splice(box.spec.text, operation, issue);
|
|
@@ -42,6 +45,8 @@ export const replaceText = {
|
|
|
42
45
|
issue(problem.path, problem.code, problem.message);
|
|
43
46
|
},
|
|
44
47
|
apply(operation, context) {
|
|
48
|
+
if (context.paragraph(operation.target)?.paragraph.id === operation.target)
|
|
49
|
+
return replaceParagraphText.apply({ ...operation, op: "replaceParagraphText" }, context);
|
|
45
50
|
const box = textBoxTarget(operation.target, context);
|
|
46
51
|
if (box)
|
|
47
52
|
return textBoxReplaceText.apply(wholeTextOperation(operation, splice(box.spec.text, operation)), context);
|
|
@@ -95,6 +100,8 @@ function spanOf(operation, previous) {
|
|
|
95
100
|
}
|
|
96
101
|
export const setTextStyle = {
|
|
97
102
|
validate(operation, context, issue) {
|
|
103
|
+
if (context.paragraph(operation.target)?.paragraph.id === operation.target)
|
|
104
|
+
return paragraphSetTextStyle.validate(operation, context, issue);
|
|
98
105
|
if (textBoxTarget(operation.target, context))
|
|
99
106
|
return textBoxSetTextStyle.validate(operation, context, issue);
|
|
100
107
|
if (!textTarget(operation.target, context, issue))
|
|
@@ -110,6 +117,8 @@ export const setTextStyle = {
|
|
|
110
117
|
issue(`/style/${field}`, "unsupported-style", `Only color and fontSize can change on existing text; ${field} cannot`);
|
|
111
118
|
},
|
|
112
119
|
apply(operation, context) {
|
|
120
|
+
if (context.paragraph(operation.target)?.paragraph.id === operation.target)
|
|
121
|
+
return paragraphSetTextStyle.apply(operation, context);
|
|
113
122
|
if (textBoxTarget(operation.target, context))
|
|
114
123
|
return textBoxSetTextStyle.apply(operation, context);
|
|
115
124
|
const target = textTarget(operation.target, context);
|
|
@@ -139,72 +148,18 @@ function textTarget(target, context, issue = () => { }) {
|
|
|
139
148
|
}
|
|
140
149
|
return { location, element };
|
|
141
150
|
}
|
|
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
151
|
/**
|
|
156
152
|
* Whether the object's own font can draw `text`: a standard font for WinAnsi
|
|
157
|
-
* text, or an embedded TrueType program
|
|
158
|
-
* (which also answers for subsets, since
|
|
159
|
-
* kept).
|
|
153
|
+
* text, or an embedded TrueType or bare CFF program with a glyph for every
|
|
154
|
+
* character (which also answers for subsets, since they hold only what they
|
|
155
|
+
* kept), each with a width in the PDF.
|
|
160
156
|
*/
|
|
161
157
|
function canKeepFont(context, target, text) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
|
158
|
+
return context.readPage(target.location.pageIndex, (page) => {
|
|
159
|
+
const object = context.pdfium.lib.FPDFPage_GetObject(page, target.location.indexes[0]);
|
|
160
|
+
return fontCanRewrite(context.pdfium, context.pdfium.lib.FPDFTextObj_GetFont(object), text);
|
|
181
161
|
});
|
|
182
162
|
}
|
|
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
163
|
/** Sets the text and confirms PDFium reads it back; false means it must be replaced. */
|
|
209
164
|
function replaceInPlace(context, target, text) {
|
|
210
165
|
const { pdfium } = context;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditWorkerFont } from "../../../worker-protocol.js";
|
|
2
|
+
import type { Issue } from "./operations.js";
|
|
2
3
|
import type { Pdfium } from "./pdfium.js";
|
|
3
4
|
export declare const FALLBACK_FAMILY = "Noto Sans";
|
|
4
5
|
export declare function isStandardFamily(family: string): boolean;
|
|
@@ -76,3 +77,16 @@ export declare class TextMeasurer {
|
|
|
76
77
|
descent: number;
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
/** True only when the original native font is known to encode every character. */
|
|
81
|
+
export declare function fontCanDraw(pdfium: Pdfium, font: number, text: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether one text object can be rewritten in its own font: what
|
|
84
|
+
* `fontCanDraw` proves, extended to bare CFF subsets, whose glyph names say
|
|
85
|
+
* what they draw, and narrowed to characters the PDF gives an advance, since
|
|
86
|
+
* a subset's widths list only the codes it used. Paragraph reflow keeps to
|
|
87
|
+
* `fontCanDraw`, which also decides which rows join a paragraph.
|
|
88
|
+
*/
|
|
89
|
+
export declare function fontCanRewrite(pdfium: Pdfium, font: number, text: string): boolean;
|
|
90
|
+
export declare function fontBytes(pdfium: Pdfium, font: number): Uint8Array | undefined;
|
|
91
|
+
/** Right-to-left scripts need shaping the MVP does not do. */
|
|
92
|
+
export declare function validateScript(text: string, issue: Issue): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { glyphUnicode, readCff } from "./cff.js";
|
|
1
2
|
/*
|
|
2
3
|
* Fonts for text web-doc writes into a PDF: the standard 14 fonts for
|
|
3
4
|
* WinAnsi text, host-registered TrueType fonts, and a bundled Noto Sans
|
|
@@ -467,3 +468,116 @@ export class TextMeasurer {
|
|
|
467
468
|
}
|
|
468
469
|
}
|
|
469
470
|
}
|
|
471
|
+
/** True only when the original native font is known to encode every character. */
|
|
472
|
+
export function fontCanDraw(pdfium, font, text) {
|
|
473
|
+
const { lib } = pdfium;
|
|
474
|
+
if (!lib.FPDFFont_GetIsEmbedded(font)) {
|
|
475
|
+
const base = pdfium.readUtf8String((buffer, bytes) => lib.FPDFFont_GetBaseFontName(font, buffer, bytes));
|
|
476
|
+
return (isStandardFamily(base.split(/[-,]/)[0] ?? "") &&
|
|
477
|
+
firstNonWinAnsi(text) === undefined);
|
|
478
|
+
}
|
|
479
|
+
const coverage = fontCoverage(pdfium, font);
|
|
480
|
+
if (!coverage)
|
|
481
|
+
return false;
|
|
482
|
+
return [...text].every((character) => {
|
|
483
|
+
const code = character.codePointAt(0);
|
|
484
|
+
// A space needs an encoding even though a valid space has no outline.
|
|
485
|
+
// Imported subsets can omit its cmap entry and use positioned word gaps.
|
|
486
|
+
if (code === 0x20)
|
|
487
|
+
return (coverage.glyph(code) ?? 0) !== 0;
|
|
488
|
+
return code === 0x0a || coverage.drawable(code);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Whether one text object can be rewritten in its own font: what
|
|
493
|
+
* `fontCanDraw` proves, extended to bare CFF subsets, whose glyph names say
|
|
494
|
+
* what they draw, and narrowed to characters the PDF gives an advance, since
|
|
495
|
+
* a subset's widths list only the codes it used. Paragraph reflow keeps to
|
|
496
|
+
* `fontCanDraw`, which also decides which rows join a paragraph.
|
|
497
|
+
*/
|
|
498
|
+
export function fontCanRewrite(pdfium, font, text) {
|
|
499
|
+
const characters = [...text].filter((character) => character !== "\n");
|
|
500
|
+
if (!characters.every((character) => advance(pdfium, font, character.codePointAt(0)) > 0))
|
|
501
|
+
return false;
|
|
502
|
+
if (fontCanDraw(pdfium, font, text))
|
|
503
|
+
return true;
|
|
504
|
+
if (!pdfium.lib.FPDFFont_GetIsEmbedded(font))
|
|
505
|
+
return false;
|
|
506
|
+
const data = fontBytes(pdfium, font);
|
|
507
|
+
const coverage = data && cffCoverage(data);
|
|
508
|
+
if (!coverage)
|
|
509
|
+
return false;
|
|
510
|
+
// A space needs a glyph of its own, as in `fontCanDraw`.
|
|
511
|
+
return characters.every((character) => coverage.has(character.codePointAt(0)));
|
|
512
|
+
}
|
|
513
|
+
/** The PDF's width for a character, per 1000 units of size; 0 for one it gives none or PDFium cannot ask about. */
|
|
514
|
+
function advance(pdfium, font, code) {
|
|
515
|
+
// PDFium takes one UTF-16 code unit.
|
|
516
|
+
if (code > 0xffff)
|
|
517
|
+
return 0;
|
|
518
|
+
return (pdfium.readNumbers(1, "float", ([width]) => pdfium.lib.FPDFFont_GetGlyphWidth(font, code, 1000, width))?.[0] ?? 0);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* A bare CFF program's glyphs by the code points their names stand for, as a
|
|
522
|
+
* simple font draws them: its encoding names the glyph. A CID-keyed program
|
|
523
|
+
* names none, and only its PDF CMap would say what its glyphs draw.
|
|
524
|
+
*/
|
|
525
|
+
function cffCoverage(data) {
|
|
526
|
+
const cff = readCff(data);
|
|
527
|
+
if (!cff || cff.cid)
|
|
528
|
+
return undefined;
|
|
529
|
+
const glyphs = new Map();
|
|
530
|
+
cff.glyphNames.forEach((name, glyph) => {
|
|
531
|
+
const code = glyphUnicode(name);
|
|
532
|
+
if (glyph !== 0 && code !== undefined && !glyphs.has(code))
|
|
533
|
+
glyphs.set(code, glyph);
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
has: (code) => glyphs.has(code),
|
|
537
|
+
glyph: (code) => glyphs.get(code),
|
|
538
|
+
drawable: (code) => glyphs.has(code),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function fontCoverage(pdfium, font) {
|
|
542
|
+
const data = fontBytes(pdfium, font);
|
|
543
|
+
if (!data)
|
|
544
|
+
return undefined;
|
|
545
|
+
const tag = String.fromCharCode(...data.slice(0, 4));
|
|
546
|
+
const truetype = (data[0] === 0 && data[1] === 1 && data[2] === 0 && data[3] === 0) ||
|
|
547
|
+
tag === "true";
|
|
548
|
+
return truetype ? parseCmap(data) : undefined;
|
|
549
|
+
}
|
|
550
|
+
export function fontBytes(pdfium, font) {
|
|
551
|
+
const { lib } = pdfium;
|
|
552
|
+
const size = pdfium.readNumbers(1, "i32", ([out]) => lib.FPDFFont_GetFontData(font, 0, 0, out))?.[0];
|
|
553
|
+
if (!size)
|
|
554
|
+
return undefined;
|
|
555
|
+
const buffer = pdfium.malloc(size);
|
|
556
|
+
try {
|
|
557
|
+
const out = pdfium.malloc(4);
|
|
558
|
+
try {
|
|
559
|
+
if (!lib.FPDFFont_GetFontData(font, buffer, size, out))
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
pdfium.free(out);
|
|
564
|
+
}
|
|
565
|
+
return pdfium.readBytes(buffer, size);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
pdfium.free(buffer);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/** Right-to-left scripts need shaping the MVP does not do. */
|
|
572
|
+
export function validateScript(text, issue) {
|
|
573
|
+
for (const character of text) {
|
|
574
|
+
const code = character.codePointAt(0);
|
|
575
|
+
if ((code >= 0x0590 && code <= 0x08ff) ||
|
|
576
|
+
(code >= 0xfb1d && code <= 0xfdff) ||
|
|
577
|
+
(code >= 0xfe70 && code <= 0xfeff)) {
|
|
578
|
+
issue("/text", "unsupported-script", `Right-to-left text such as "${character}" cannot be edited yet`);
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
@@ -84,6 +84,8 @@ export function createPdfEditHandler(host) {
|
|
|
84
84
|
}
|
|
85
85
|
case "edit-text-layout":
|
|
86
86
|
return engine().textLayout(payload.id);
|
|
87
|
+
case "edit-text-paragraph":
|
|
88
|
+
return engine().textParagraph(payload.id);
|
|
87
89
|
case "edit-position-at": {
|
|
88
90
|
const { pageIndex, point } = payload;
|
|
89
91
|
return engine().positionAt(pageIndex, point);
|
|
@@ -2,7 +2,7 @@ import { textStyle } from "./elements.js";
|
|
|
2
2
|
import { pageToUser, rectContains, round, roundRect, unionRects, userRectToPage, userToPage, } from "./geometry.js";
|
|
3
3
|
/** How far a point may be from a glyph for `FPDFText_GetCharIndexAtPos`. */
|
|
4
4
|
const HIT_TOLERANCE = 2;
|
|
5
|
-
const TEXT_KINDS = new Set(["text", "textBox", "table"]);
|
|
5
|
+
const TEXT_KINDS = new Set(["text", "textBox", "table", "paragraph"]);
|
|
6
6
|
/** The layout of one element, or `undefined` when it draws no text. */
|
|
7
7
|
export function layoutOf(pdfium, scan, element) {
|
|
8
8
|
if (!TEXT_KINDS.has(element.kind))
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ResourceLimits, ViewerWarning } from "../../../contracts.js";
|
|
2
2
|
import type { AssetSource } from "../../assets.js";
|
|
3
3
|
import type { ImageCache } from "./images.js";
|
|
4
|
-
import type { OperationIssue } from "../../types.js";
|
|
4
|
+
import type { OperationIssue, TextAnchorMigration } from "../../types.js";
|
|
5
5
|
import type { PdfElement, PdfOperation } from "../types.js";
|
|
6
6
|
import type { PageGeometry } from "./geometry.js";
|
|
7
7
|
import type { ObjectRecord } from "./elements.js";
|
|
8
8
|
import type { Pdfium } from "./pdfium.js";
|
|
9
9
|
import type { FontLibrary, TextMeasurer } from "./fonts.js";
|
|
10
|
+
import type { ParagraphTarget } from "./paragraph.js";
|
|
10
11
|
/** What an operation sees of the document while validating or applying. */
|
|
11
12
|
export interface OperationContext {
|
|
12
13
|
readonly pdfium: Pdfium;
|
|
@@ -24,12 +25,16 @@ export interface OperationContext {
|
|
|
24
25
|
newId(pageIndex: number, suffix?: string): string;
|
|
25
26
|
/** Loads a page for writing; its content is regenerated afterwards. */
|
|
26
27
|
withPage<T>(pageIndex: number, use: (page: number) => T): T;
|
|
28
|
+
/** Reads native resources without regenerating the content stream. */
|
|
29
|
+
readPage<T>(pageIndex: number, use: (page: number) => T): T;
|
|
27
30
|
/** Records objects appended to a page by this operation. */
|
|
28
31
|
appendObjects(pageIndex: number, records: readonly ObjectRecord[]): void;
|
|
29
32
|
/** Where an element's objects sit: their page and their indexes in drawing order. */
|
|
30
33
|
locate(id: string): ElementLocation | undefined;
|
|
31
34
|
/** The element as a query would return it. */
|
|
32
35
|
element(id: string): PdfElement | undefined;
|
|
36
|
+
paragraph(id: string): ParagraphTarget | undefined;
|
|
37
|
+
pageElements(pageIndex: number): readonly PdfElement[];
|
|
33
38
|
/** Ids of every element on a page, for what a page deletion removes. */
|
|
34
39
|
pageElementIds(pageIndex: number): readonly string[];
|
|
35
40
|
/** Displayed size of a page in points. */
|
|
@@ -56,6 +61,7 @@ export interface OperationResult {
|
|
|
56
61
|
readonly createdIds: readonly string[];
|
|
57
62
|
/** Ids the operation made disappear. */
|
|
58
63
|
readonly removedIds?: readonly string[];
|
|
64
|
+
readonly textAnchorMigrations?: readonly Omit<TextAnchorMigration, "operationIndex">[];
|
|
59
65
|
readonly changedPages: readonly number[];
|
|
60
66
|
readonly warnings: readonly ViewerWarning[];
|
|
61
67
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReplaceParagraphTextOperation, SetTextStyleOperation } from "../types.js";
|
|
2
|
+
import type { OperationHandler } from "./operations.js";
|
|
3
|
+
export declare const replaceParagraphText: OperationHandler<ReplaceParagraphTextOperation>;
|
|
4
|
+
export declare const paragraphSetTextStyle: OperationHandler<SetTextStyleOperation>;
|