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.
Files changed (58) hide show
  1. package/dist/adapters/office.js +4 -2
  2. package/dist/adapters/pdf.js +19 -8
  3. package/dist/edit/docx/engine.d.ts +9 -1
  4. package/dist/edit/docx/engine.js +22 -0
  5. package/dist/edit/docx/provider.d.ts +3 -1
  6. package/dist/edit/docx/provider.js +3 -0
  7. package/dist/edit/docx/session.d.ts +6 -2
  8. package/dist/edit/docx/session.js +7 -1
  9. package/dist/edit/docx/style.d.ts +1 -1
  10. package/dist/edit/docx/style.js +3 -2
  11. package/dist/edit/docx/types.d.ts +11 -1
  12. package/dist/edit/engine.d.ts +2 -1
  13. package/dist/edit/pdf/engine/document.d.ts +4 -2
  14. package/dist/edit/pdf/engine/document.js +115 -12
  15. package/dist/edit/pdf/engine/elements.d.ts +5 -1
  16. package/dist/edit/pdf/engine/elements.js +88 -6
  17. package/dist/edit/pdf/engine/existing-text.js +16 -61
  18. package/dist/edit/pdf/engine/fonts.d.ts +14 -0
  19. package/dist/edit/pdf/engine/fonts.js +114 -0
  20. package/dist/edit/pdf/engine/handler.js +2 -0
  21. package/dist/edit/pdf/engine/layout.js +1 -1
  22. package/dist/edit/pdf/engine/operations.d.ts +7 -1
  23. package/dist/edit/pdf/engine/paragraph-edit.d.ts +4 -0
  24. package/dist/edit/pdf/engine/paragraph-edit.js +331 -0
  25. package/dist/edit/pdf/engine/paragraph.d.ts +31 -0
  26. package/dist/edit/pdf/engine/paragraph.js +445 -0
  27. package/dist/edit/pdf/engine/pdfium.d.ts +3 -0
  28. package/dist/edit/pdf/engine/text-box.js +16 -3
  29. package/dist/edit/pdf/engine/text-font.js +2 -19
  30. package/dist/edit/pdf/engine/transform.js +14 -3
  31. package/dist/edit/pdf/provider.d.ts +3 -1
  32. package/dist/edit/pdf/provider.js +3 -0
  33. package/dist/edit/pdf/range-map.d.ts +2 -1
  34. package/dist/edit/pdf/range-map.js +38 -8
  35. package/dist/edit/pdf/schemas.d.ts +2 -0
  36. package/dist/edit/pdf/schemas.js +37 -0
  37. package/dist/edit/pdf/session.d.ts +3 -1
  38. package/dist/edit/pdf/session.js +35 -6
  39. package/dist/edit/pdf/types.d.ts +38 -8
  40. package/dist/edit/pptx/elements.d.ts +4 -1
  41. package/dist/edit/pptx/elements.js +35 -0
  42. package/dist/edit/pptx/engine.d.ts +5 -1
  43. package/dist/edit/pptx/engine.js +11 -0
  44. package/dist/edit/pptx/handler.js +8 -0
  45. package/dist/edit/pptx/provider.d.ts +3 -1
  46. package/dist/edit/pptx/provider.js +3 -0
  47. package/dist/edit/pptx/session.d.ts +6 -2
  48. package/dist/edit/pptx/session.js +7 -1
  49. package/dist/edit/pptx/types.d.ts +11 -1
  50. package/dist/edit/range-style.d.ts +24 -0
  51. package/dist/edit/range-style.js +46 -0
  52. package/dist/edit/session.js +5 -0
  53. package/dist/edit/types.d.ts +13 -0
  54. package/dist/viewport.js +11 -0
  55. package/dist/worker-protocol.d.ts +1 -1
  56. package/dist/workers/ooxml-edit-worker.js +90 -2
  57. package/dist/workers/pdf-edit-worker.js +2019 -1003
  58. 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 === "textBox") {
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 { firstNonWinAnsi, isStandardFamily, parseCmap, } from "./fonts.js";
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 whose cmap covers every character
158
- * (which also answers for subsets, since their cmaps hold only what they
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
- 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;
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>;