web-doc 0.8.0 → 0.9.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/edit/ai/tools.js +3 -1
- package/dist/edit/docx/elements.js +19 -1
- package/dist/edit/docx/types.d.ts +8 -0
- package/dist/edit/pdf/engine/document.js +48 -3
- package/dist/edit/pdf/engine/elements.d.ts +6 -0
- package/dist/edit/pdf/engine/fonts.js +13 -10
- package/dist/edit/pdf/engine/pages.js +9 -1
- package/dist/edit/pdf/schemas.js +4 -1
- package/dist/edit/pdf/types.d.ts +15 -3
- package/dist/fonts/THIRD_PARTY_NOTICES.md +6 -3
- package/dist/fonts/manifest.json +4 -4
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/workers/ooxml-edit-worker.js +15 -1
- package/dist/workers/pdf-edit-worker.js +60 -15
- package/package.json +1 -1
package/dist/edit/ai/tools.js
CHANGED
|
@@ -580,7 +580,9 @@ function describeOperation(format, pages, operation, id) {
|
|
|
580
580
|
case "moveSlide":
|
|
581
581
|
return `Moved ${pages} ${Number(fields.from) + 1} to ${Number(fields.to) + 1}`;
|
|
582
582
|
case "rotatePage":
|
|
583
|
-
return
|
|
583
|
+
return fields.by === undefined
|
|
584
|
+
? `Rotated ${pages} ${Number(fields.pageIndex) + 1} to ${String(fields.rotation)}°`
|
|
585
|
+
: `Turned ${pages} ${Number(fields.pageIndex) + 1} by ${String(fields.by)}°`;
|
|
584
586
|
default:
|
|
585
587
|
return `Applied ${operation.op}${target === "the element" ? "" : ` to ${target}`}${tag}`;
|
|
586
588
|
}
|
|
@@ -81,10 +81,28 @@ function paragraphElement(model, record, base) {
|
|
|
81
81
|
id: record.elementId,
|
|
82
82
|
kind: "paragraph",
|
|
83
83
|
text: record.text.text,
|
|
84
|
-
...(record.table
|
|
84
|
+
...(record.table
|
|
85
|
+
? { parentId: record.table.elementId, ...cellOf(record.table, record) }
|
|
86
|
+
: {}),
|
|
85
87
|
textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
|
|
86
88
|
paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
|
|
87
89
|
...(record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {}),
|
|
88
90
|
operations: operationsOf("paragraph", record.readOnlyReason !== undefined),
|
|
89
91
|
};
|
|
90
92
|
}
|
|
93
|
+
/** Where each paragraph of a table sits, read once per table. */
|
|
94
|
+
const CELLS = new WeakMap();
|
|
95
|
+
/** The cell of a table that holds a paragraph, when the table lists it. */
|
|
96
|
+
function cellOf(table, record) {
|
|
97
|
+
let cells = CELLS.get(table);
|
|
98
|
+
if (!cells) {
|
|
99
|
+
cells = new Map();
|
|
100
|
+
for (const [row, cellsOfRow] of table.rows.entries())
|
|
101
|
+
for (const [column, cell] of cellsOfRow.entries())
|
|
102
|
+
for (const paragraph of cell.paragraphs)
|
|
103
|
+
cells.set(paragraph, { row, column });
|
|
104
|
+
CELLS.set(table, cells);
|
|
105
|
+
}
|
|
106
|
+
const cell = cells.get(record);
|
|
107
|
+
return cell ? { cell } : {};
|
|
108
|
+
}
|
|
@@ -54,6 +54,14 @@ export interface DocxElement extends EditElement {
|
|
|
54
54
|
readonly textStyle?: DocxTextStyle;
|
|
55
55
|
/** Present for a paragraph. */
|
|
56
56
|
readonly paragraphStyle?: DocxParagraphStyle;
|
|
57
|
+
/**
|
|
58
|
+
* Present for a paragraph in a table cell: the cell's row and its place in
|
|
59
|
+
* the row, both from 0. Paragraphs move only among those of the same cell.
|
|
60
|
+
*/
|
|
61
|
+
readonly cell?: {
|
|
62
|
+
readonly row: number;
|
|
63
|
+
readonly column: number;
|
|
64
|
+
};
|
|
57
65
|
/** Present for a table: cell text by row. */
|
|
58
66
|
readonly table?: {
|
|
59
67
|
readonly rows: readonly (readonly string[])[];
|
|
@@ -535,8 +535,11 @@ export class PdfEditDocument {
|
|
|
535
535
|
},
|
|
536
536
|
// Ids name the state the batch leads to, the operation and the item,
|
|
537
537
|
// so replaying the history reproduces them and an undone id is never
|
|
538
|
-
// handed out again.
|
|
539
|
-
|
|
538
|
+
// handed out again. A file saved by an earlier session carries the ids
|
|
539
|
+
// that session numbered from 1 as well, so one already on the page
|
|
540
|
+
// gets the first free `~n` instead (ACTION-886); the saved objects are
|
|
541
|
+
// part of the base, so a replay meets them and picks the same id.
|
|
542
|
+
newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
|
|
540
543
|
withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
|
|
541
544
|
appendObjects: (pageIndex, records) => {
|
|
542
545
|
const page = this.#pages[pageIndex];
|
|
@@ -686,10 +689,52 @@ export class PdfEditDocument {
|
|
|
686
689
|
}
|
|
687
690
|
}
|
|
688
691
|
record.objects = objects.map((object, index) => object.mark && stale.has(object.id)
|
|
689
|
-
? {
|
|
692
|
+
? {
|
|
693
|
+
id: `${record.key}:o${index}`,
|
|
694
|
+
type: object.type,
|
|
695
|
+
staleMarkId: object.id,
|
|
696
|
+
}
|
|
690
697
|
: object);
|
|
691
698
|
return record.objects;
|
|
692
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* `id`, or its first free `~n` when an object saved by an earlier session
|
|
702
|
+
* has it already. Only ids a session numbered (`<key>:n…`) can collide, so
|
|
703
|
+
* only those are gathered; the objects are read without the page's text.
|
|
704
|
+
*/
|
|
705
|
+
#unusedId(pageIndex, id) {
|
|
706
|
+
const record = this.#pages[pageIndex];
|
|
707
|
+
const objects = record.objects ?? this.#loadObjects(pageIndex);
|
|
708
|
+
const numbered = `${record.key}:n`;
|
|
709
|
+
const taken = new Set();
|
|
710
|
+
for (const object of objects) {
|
|
711
|
+
if (object.id.startsWith(numbered))
|
|
712
|
+
taken.add(object.id);
|
|
713
|
+
if (object.staleMarkId?.startsWith(numbered))
|
|
714
|
+
taken.add(object.staleMarkId);
|
|
715
|
+
}
|
|
716
|
+
if (!taken.has(id))
|
|
717
|
+
return id;
|
|
718
|
+
let suffix = 1;
|
|
719
|
+
while (taken.has(`${id}~${suffix}`))
|
|
720
|
+
suffix += 1;
|
|
721
|
+
return `${id}~${suffix}`;
|
|
722
|
+
}
|
|
723
|
+
/** A page's objects, loading the page alone when they are not known yet. */
|
|
724
|
+
#loadObjects(pageIndex) {
|
|
725
|
+
const { lib } = this.#pdfium;
|
|
726
|
+
const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
727
|
+
if (!page)
|
|
728
|
+
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
729
|
+
details: { pageIndex },
|
|
730
|
+
});
|
|
731
|
+
try {
|
|
732
|
+
return this.#objectsOf(pageIndex, page);
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
lib.FPDF_ClosePage(page);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
693
738
|
/** Marks from other sessions keep their id only if it cannot collide with ours. */
|
|
694
739
|
#ownMark(mark, pageKey) {
|
|
695
740
|
return mark.id.startsWith(`${pageKey}:`)
|
|
@@ -21,6 +21,12 @@ export interface ObjectRecord {
|
|
|
21
21
|
readonly id: string;
|
|
22
22
|
readonly type: number;
|
|
23
23
|
readonly mark?: MarkParams;
|
|
24
|
+
/**
|
|
25
|
+
* The id a mark the file still carries names, on an object listed as plain
|
|
26
|
+
* because the mark failed its check: a new id must not take it, or saving
|
|
27
|
+
* would join the new object to this one.
|
|
28
|
+
*/
|
|
29
|
+
readonly staleMarkId?: string;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Reads the objects of a loaded page in drawing order and turns them into
|
|
@@ -110,7 +110,13 @@ export class FontLibrary {
|
|
|
110
110
|
return undefined;
|
|
111
111
|
if (this.#fallback && this.#covers(this.#fallback, request.text))
|
|
112
112
|
return undefined;
|
|
113
|
-
|
|
113
|
+
// Name the character that actually stops the text: the first one the
|
|
114
|
+
// fallback lacks when it was tried, else the first the standard fonts
|
|
115
|
+
// cannot encode. Naming the first Cyrillic letter for a dash the
|
|
116
|
+
// fallback lacked sent people after the wrong character (ACTION-879).
|
|
117
|
+
const bad = this.#fallback
|
|
118
|
+
? this.#firstMissing(this.#fallback, request.text)
|
|
119
|
+
: firstNonWinAnsi(request.text);
|
|
114
120
|
return {
|
|
115
121
|
path: "/text",
|
|
116
122
|
code: "font-unavailable",
|
|
@@ -175,6 +181,10 @@ export class FontLibrary {
|
|
|
175
181
|
return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
|
|
176
182
|
}
|
|
177
183
|
#covers(bytes, text) {
|
|
184
|
+
return this.#firstMissing(bytes, text) === undefined;
|
|
185
|
+
}
|
|
186
|
+
/** The first character of `text` the font has no glyph for; layout handles line breaks and spaces. */
|
|
187
|
+
#firstMissing(bytes, text) {
|
|
178
188
|
let coverage = this.#coverage.get(bytes);
|
|
179
189
|
if (!coverage) {
|
|
180
190
|
coverage = parseCmap(bytes);
|
|
@@ -185,9 +195,9 @@ export class FontLibrary {
|
|
|
185
195
|
if (code === 0x0a || code === 0x20)
|
|
186
196
|
continue;
|
|
187
197
|
if (!coverage.has(code))
|
|
188
|
-
return
|
|
198
|
+
return character;
|
|
189
199
|
}
|
|
190
|
-
return
|
|
200
|
+
return undefined;
|
|
191
201
|
}
|
|
192
202
|
#load(pdfium, document, bytes) {
|
|
193
203
|
let handles = this.#handles.get(document);
|
|
@@ -242,13 +252,6 @@ function isBold(font) {
|
|
|
242
252
|
function isItalic(font) {
|
|
243
253
|
return font.style !== "normal";
|
|
244
254
|
}
|
|
245
|
-
function firstUncovered(text, library) {
|
|
246
|
-
void library;
|
|
247
|
-
for (const character of text)
|
|
248
|
-
if (character !== "\n" && character !== " ")
|
|
249
|
-
return character;
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
255
|
/** The code points a TrueType font maps to glyphs, from its cmap table. */
|
|
253
256
|
/**
|
|
254
257
|
* Whether glyphs of a TrueType font have outline data: a subset font can keep
|
|
@@ -74,11 +74,19 @@ export const rotatePage = {
|
|
|
74
74
|
validate(operation, context, issue) {
|
|
75
75
|
if (operation.pageIndex >= context.pageCount)
|
|
76
76
|
issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
|
|
77
|
+
// A field given as undefined counts as absent, as it does everywhere else.
|
|
78
|
+
if ((operation.rotation === undefined) === (operation.by === undefined))
|
|
79
|
+
issue("", "one-of", "Give exactly one of `rotation` and `by`");
|
|
77
80
|
},
|
|
78
81
|
apply(operation, context) {
|
|
79
82
|
const { lib } = context.pdfium;
|
|
80
83
|
context.withPage(operation.pageIndex, (page) => {
|
|
81
|
-
|
|
84
|
+
// A turn reads the angle the page has now, so it composes with what
|
|
85
|
+
// the file was saved with and with turns earlier in the history.
|
|
86
|
+
const quarters = operation.by !== undefined
|
|
87
|
+
? (lib.FPDFPage_GetRotation(page) + operation.by / 90) % 4
|
|
88
|
+
: operation.rotation / 90;
|
|
89
|
+
lib.FPDFPage_SetRotation(page, quarters);
|
|
82
90
|
});
|
|
83
91
|
context.invalidatePage(operation.pageIndex);
|
|
84
92
|
return {
|
package/dist/edit/pdf/schemas.js
CHANGED
|
@@ -170,10 +170,13 @@ export const pdfOperationSchemas = Object.freeze({
|
|
|
170
170
|
from: { type: "integer", minimum: 0 },
|
|
171
171
|
to: { type: "integer", minimum: 0 },
|
|
172
172
|
}, ["from", "to"]),
|
|
173
|
+
// Exactly one of `rotation` (absolute) and `by` (a turn from the current
|
|
174
|
+
// angle); the handler reports a batch that gives both or neither.
|
|
173
175
|
rotatePage: operation("rotatePage", {
|
|
174
176
|
pageIndex: { type: "integer", minimum: 0 },
|
|
175
177
|
rotation: { enum: [0, 90, 180, 270] },
|
|
176
|
-
|
|
178
|
+
by: { enum: [90, 180, 270] },
|
|
179
|
+
}, ["pageIndex"]),
|
|
177
180
|
insertShape: operation("insertShape", {
|
|
178
181
|
pageIndex: { type: "integer", minimum: 0 },
|
|
179
182
|
shape: { enum: ["rectangle", "ellipse", "line"] },
|
package/dist/edit/pdf/types.d.ts
CHANGED
|
@@ -192,12 +192,24 @@ export interface SetShapeStyleOperation {
|
|
|
192
192
|
/** A new fill, `null` to remove it, or absent to keep it. */
|
|
193
193
|
readonly fill?: PdfFill | null;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Sets a page's rotation, or turns it from where it stands: a host offering
|
|
197
|
+
* "rotate" does not need to know the angle a file was saved with, and a
|
|
198
|
+
* replay of the history turns from the same angle again.
|
|
199
|
+
*/
|
|
200
|
+
export type RotatePageOperation = {
|
|
196
201
|
readonly op: "rotatePage";
|
|
197
202
|
readonly pageIndex: number;
|
|
198
203
|
/** Absolute clockwise rotation in degrees. */
|
|
199
204
|
readonly rotation: 0 | 90 | 180 | 270;
|
|
200
|
-
|
|
205
|
+
readonly by?: never;
|
|
206
|
+
} | {
|
|
207
|
+
readonly op: "rotatePage";
|
|
208
|
+
readonly pageIndex: number;
|
|
209
|
+
/** Clockwise turn in degrees from the page's current rotation. */
|
|
210
|
+
readonly by: 90 | 180 | 270;
|
|
211
|
+
readonly rotation?: never;
|
|
212
|
+
};
|
|
201
213
|
/** The PDF operation union; operations are added as they ship. */
|
|
202
214
|
export type PdfOperation = InsertTextBoxOperation | ReplaceTextOperation | SetTextStyleOperation | ResizeElementOperation | MoveElementOperation | DeleteElementOperation | InsertPageOperation | DeletePageOperation | MovePageOperation | RotatePageOperation | InsertShapeOperation | SetShapeStyleOperation | InsertImageOperation | InsertTableOperation | SetTableCellOperation;
|
|
203
215
|
export interface PdfSaveOptions extends SaveOptions {
|
|
@@ -321,7 +333,7 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
|
|
|
321
333
|
deletePage(fields: Fields<DeletePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
322
334
|
/** Reorders pages. */
|
|
323
335
|
movePage(fields: Fields<MovePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
324
|
-
/** Sets a page's rotation. */
|
|
336
|
+
/** Sets a page's rotation, or turns it from the one it has. */
|
|
325
337
|
rotatePage(fields: Fields<RotatePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
326
338
|
/** Draws a rectangle, ellipse or line as a path object. */
|
|
327
339
|
insertShape(fields: Fields<InsertShapeOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
@@ -6,8 +6,11 @@ The WOFF2 files in this directory are modified/subset builds of Noto Sans and No
|
|
|
6
6
|
- Noto Sans CJK sources: `notofonts/noto-cjk` commit `f8d157532fbfaeda587e826d4cd5b21a49186f7c`.
|
|
7
7
|
- License: SIL Open Font License 1.1; see `OFL-1.1.txt`.
|
|
8
8
|
- Modified font family used by the viewer: `Zrimo Noto`.
|
|
9
|
-
- `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset plus Noto
|
|
10
|
+
Sans's General Punctuation, Superscripts and Subscripts, Currency Symbols,
|
|
11
|
+
Letterlike Symbols and Number Forms (ranges in `manifest.json`; the Arrows
|
|
12
|
+
and Mathematical Operators ranges are requested too, but Noto Sans has no
|
|
13
|
+
glyphs there), saved as an uncompressed TrueType file; the PDF editor embeds it into
|
|
14
|
+
documents whose text the standard fonts cannot encode.
|
|
12
15
|
|
|
13
16
|
No proprietary Microsoft font is included.
|
package/dist/fonts/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"family": "Zrimo Noto",
|
|
4
|
-
"generated": "2026-
|
|
5
|
-
"tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs",
|
|
4
|
+
"generated": "2026-10-02",
|
|
5
|
+
"tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs, from hinted/ttf/NotoSans/NotoSans-Regular.ttf with U+0000,U+000D,U+0020-007E,U+00A0-024F,U+0300-036F,U+0400-052F,U+1E00-1EFF plus General Punctuation, Superscripts and Subscripts, Currency Symbols, Letterlike Symbols, Number Forms, Arrows, Mathematical Operators, U+25CC and U+FFFD (U+2000-206F,U+2070-209F,U+20A0-20CF,U+2100-214F,U+2150-218F,U+2190-21FF,U+2200-22FF,U+25CC,U+FFFD) so text typed with dashes, quotes, currency signs and the numero sign can be embedded; Noto Sans has no glyphs in the Arrows and Mathematical Operators ranges, so those two add none",
|
|
6
6
|
"license": "OFL-1.1",
|
|
7
7
|
"sources": {
|
|
8
8
|
"noto-fonts": "https://github.com/notofonts/noto-fonts/tree/ffebf8c1ee449e544955a7e813c54f9b73848eac",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"file": "noto-sans-latin-cyrillic.ttf",
|
|
84
|
-
"bytes":
|
|
85
|
-
"sha256": "
|
|
84
|
+
"bytes": 260552,
|
|
85
|
+
"sha256": "94094981da300fdb704d64cb91354360d37bf69b26f8584da7d44fdc2e0bdc84"
|
|
86
86
|
}
|
|
87
87
|
]
|
|
88
88
|
}
|
|
Binary file
|
|
@@ -3300,13 +3300,27 @@ function paragraphElement(model, record, base) {
|
|
|
3300
3300
|
id: record.elementId,
|
|
3301
3301
|
kind: "paragraph",
|
|
3302
3302
|
text: record.text.text,
|
|
3303
|
-
...record.table ? { parentId: record.table.elementId } : {},
|
|
3303
|
+
...record.table ? { parentId: record.table.elementId, ...cellOf(record.table, record) } : {},
|
|
3304
3304
|
textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
|
|
3305
3305
|
paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
|
|
3306
3306
|
...record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {},
|
|
3307
3307
|
operations: operationsOf("paragraph", record.readOnlyReason !== void 0)
|
|
3308
3308
|
};
|
|
3309
3309
|
}
|
|
3310
|
+
var CELLS = /* @__PURE__ */ new WeakMap();
|
|
3311
|
+
function cellOf(table, record) {
|
|
3312
|
+
let cells = CELLS.get(table);
|
|
3313
|
+
if (!cells) {
|
|
3314
|
+
cells = /* @__PURE__ */ new Map();
|
|
3315
|
+
for (const [row, cellsOfRow] of table.rows.entries())
|
|
3316
|
+
for (const [column, cell2] of cellsOfRow.entries())
|
|
3317
|
+
for (const paragraph of cell2.paragraphs)
|
|
3318
|
+
cells.set(paragraph, { row, column });
|
|
3319
|
+
CELLS.set(table, cells);
|
|
3320
|
+
}
|
|
3321
|
+
const cell = cells.get(record);
|
|
3322
|
+
return cell ? { cell } : {};
|
|
3323
|
+
}
|
|
3310
3324
|
|
|
3311
3325
|
// src/edit/docx/write.ts
|
|
3312
3326
|
var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
@@ -5850,13 +5850,16 @@ var pdfOperationSchemas = Object.freeze({
|
|
|
5850
5850
|
},
|
|
5851
5851
|
["from", "to"]
|
|
5852
5852
|
),
|
|
5853
|
+
// Exactly one of `rotation` (absolute) and `by` (a turn from the current
|
|
5854
|
+
// angle); the handler reports a batch that gives both or neither.
|
|
5853
5855
|
rotatePage: operation(
|
|
5854
5856
|
"rotatePage",
|
|
5855
5857
|
{
|
|
5856
5858
|
pageIndex: { type: "integer", minimum: 0 },
|
|
5857
|
-
rotation: { enum: [0, 90, 180, 270] }
|
|
5859
|
+
rotation: { enum: [0, 90, 180, 270] },
|
|
5860
|
+
by: { enum: [90, 180, 270] }
|
|
5858
5861
|
},
|
|
5859
|
-
["pageIndex"
|
|
5862
|
+
["pageIndex"]
|
|
5860
5863
|
),
|
|
5861
5864
|
insertShape: operation(
|
|
5862
5865
|
"insertShape",
|
|
@@ -6897,7 +6900,7 @@ var FontLibrary = class {
|
|
|
6897
6900
|
if (this.#coveringRegistered(request)) return void 0;
|
|
6898
6901
|
if (this.#fallback && this.#covers(this.#fallback, request.text))
|
|
6899
6902
|
return void 0;
|
|
6900
|
-
const bad =
|
|
6903
|
+
const bad = this.#fallback ? this.#firstMissing(this.#fallback, request.text) : firstNonWinAnsi(request.text);
|
|
6901
6904
|
return {
|
|
6902
6905
|
path: "/text",
|
|
6903
6906
|
code: "font-unavailable",
|
|
@@ -6955,6 +6958,10 @@ var FontLibrary = class {
|
|
|
6955
6958
|
return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
|
|
6956
6959
|
}
|
|
6957
6960
|
#covers(bytes, text) {
|
|
6961
|
+
return this.#firstMissing(bytes, text) === void 0;
|
|
6962
|
+
}
|
|
6963
|
+
/** The first character of `text` the font has no glyph for; layout handles line breaks and spaces. */
|
|
6964
|
+
#firstMissing(bytes, text) {
|
|
6958
6965
|
let coverage = this.#coverage.get(bytes);
|
|
6959
6966
|
if (!coverage) {
|
|
6960
6967
|
coverage = parseCmap(bytes);
|
|
@@ -6963,9 +6970,9 @@ var FontLibrary = class {
|
|
|
6963
6970
|
for (const character of text) {
|
|
6964
6971
|
const code = character.codePointAt(0);
|
|
6965
6972
|
if (code === 10 || code === 32) continue;
|
|
6966
|
-
if (!coverage.has(code)) return
|
|
6973
|
+
if (!coverage.has(code)) return character;
|
|
6967
6974
|
}
|
|
6968
|
-
return
|
|
6975
|
+
return void 0;
|
|
6969
6976
|
}
|
|
6970
6977
|
#load(pdfium, document2, bytes) {
|
|
6971
6978
|
let handles = this.#handles.get(document2);
|
|
@@ -7017,12 +7024,6 @@ function isBold(font) {
|
|
|
7017
7024
|
function isItalic(font) {
|
|
7018
7025
|
return font.style !== "normal";
|
|
7019
7026
|
}
|
|
7020
|
-
function firstUncovered(text, library) {
|
|
7021
|
-
void library;
|
|
7022
|
-
for (const character of text)
|
|
7023
|
-
if (character !== "\n" && character !== " ") return character;
|
|
7024
|
-
return void 0;
|
|
7025
|
-
}
|
|
7026
7027
|
function glyphOutlines(view, tables) {
|
|
7027
7028
|
const head = tables.get("head");
|
|
7028
7029
|
const maxp = tables.get("maxp");
|
|
@@ -8663,11 +8664,14 @@ var rotatePage = {
|
|
|
8663
8664
|
validate(operation2, context, issue) {
|
|
8664
8665
|
if (operation2.pageIndex >= context.pageCount)
|
|
8665
8666
|
issue("/pageIndex", "unknown-target", `No page ${operation2.pageIndex}`);
|
|
8667
|
+
if (operation2.rotation === void 0 === (operation2.by === void 0))
|
|
8668
|
+
issue("", "one-of", "Give exactly one of `rotation` and `by`");
|
|
8666
8669
|
},
|
|
8667
8670
|
apply(operation2, context) {
|
|
8668
8671
|
const { lib } = context.pdfium;
|
|
8669
8672
|
context.withPage(operation2.pageIndex, (page) => {
|
|
8670
|
-
lib.
|
|
8673
|
+
const quarters = operation2.by !== void 0 ? (lib.FPDFPage_GetRotation(page) + operation2.by / 90) % 4 : operation2.rotation / 90;
|
|
8674
|
+
lib.FPDFPage_SetRotation(page, quarters);
|
|
8671
8675
|
});
|
|
8672
8676
|
context.invalidatePage(operation2.pageIndex);
|
|
8673
8677
|
return {
|
|
@@ -10043,8 +10047,11 @@ var PdfEditDocument = class {
|
|
|
10043
10047
|
},
|
|
10044
10048
|
// Ids name the state the batch leads to, the operation and the item,
|
|
10045
10049
|
// so replaying the history reproduces them and an undone id is never
|
|
10046
|
-
// handed out again.
|
|
10047
|
-
|
|
10050
|
+
// handed out again. A file saved by an earlier session carries the ids
|
|
10051
|
+
// that session numbered from 1 as well, so one already on the page
|
|
10052
|
+
// gets the first free `~n` instead (ACTION-886); the saved objects are
|
|
10053
|
+
// part of the base, so a replay meets them and picks the same id.
|
|
10054
|
+
newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
|
|
10048
10055
|
withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
|
|
10049
10056
|
appendObjects: (pageIndex, records) => {
|
|
10050
10057
|
const page = this.#pages[pageIndex];
|
|
@@ -10200,10 +10207,48 @@ var PdfEditDocument = class {
|
|
|
10200
10207
|
}
|
|
10201
10208
|
}
|
|
10202
10209
|
record.objects = objects.map(
|
|
10203
|
-
(object, index) => object.mark && stale.has(object.id) ? {
|
|
10210
|
+
(object, index) => object.mark && stale.has(object.id) ? {
|
|
10211
|
+
id: `${record.key}:o${index}`,
|
|
10212
|
+
type: object.type,
|
|
10213
|
+
staleMarkId: object.id
|
|
10214
|
+
} : object
|
|
10204
10215
|
);
|
|
10205
10216
|
return record.objects;
|
|
10206
10217
|
}
|
|
10218
|
+
/**
|
|
10219
|
+
* `id`, or its first free `~n` when an object saved by an earlier session
|
|
10220
|
+
* has it already. Only ids a session numbered (`<key>:n…`) can collide, so
|
|
10221
|
+
* only those are gathered; the objects are read without the page's text.
|
|
10222
|
+
*/
|
|
10223
|
+
#unusedId(pageIndex, id) {
|
|
10224
|
+
const record = this.#pages[pageIndex];
|
|
10225
|
+
const objects = record.objects ?? this.#loadObjects(pageIndex);
|
|
10226
|
+
const numbered = `${record.key}:n`;
|
|
10227
|
+
const taken = /* @__PURE__ */ new Set();
|
|
10228
|
+
for (const object of objects) {
|
|
10229
|
+
if (object.id.startsWith(numbered)) taken.add(object.id);
|
|
10230
|
+
if (object.staleMarkId?.startsWith(numbered))
|
|
10231
|
+
taken.add(object.staleMarkId);
|
|
10232
|
+
}
|
|
10233
|
+
if (!taken.has(id)) return id;
|
|
10234
|
+
let suffix = 1;
|
|
10235
|
+
while (taken.has(`${id}~${suffix}`)) suffix += 1;
|
|
10236
|
+
return `${id}~${suffix}`;
|
|
10237
|
+
}
|
|
10238
|
+
/** A page's objects, loading the page alone when they are not known yet. */
|
|
10239
|
+
#loadObjects(pageIndex) {
|
|
10240
|
+
const { lib } = this.#pdfium;
|
|
10241
|
+
const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
10242
|
+
if (!page)
|
|
10243
|
+
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
10244
|
+
details: { pageIndex }
|
|
10245
|
+
});
|
|
10246
|
+
try {
|
|
10247
|
+
return this.#objectsOf(pageIndex, page);
|
|
10248
|
+
} finally {
|
|
10249
|
+
lib.FPDF_ClosePage(page);
|
|
10250
|
+
}
|
|
10251
|
+
}
|
|
10207
10252
|
/** Marks from other sessions keep their id only if it cannot collide with ours. */
|
|
10208
10253
|
#ownMark(mark, pageKey) {
|
|
10209
10254
|
return mark.id.startsWith(`${pageKey}:`) ? mark : { ...mark, id: `${pageKey}:${mark.id}` };
|