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
package/dist/adapters/pdf.js
CHANGED
|
@@ -90,14 +90,16 @@ class PdfJsBackend {
|
|
|
90
90
|
#module;
|
|
91
91
|
#document;
|
|
92
92
|
#worker;
|
|
93
|
+
#workerPort;
|
|
93
94
|
#loadingTask;
|
|
94
95
|
#maxPixels;
|
|
95
96
|
#pages = new Map();
|
|
96
97
|
#closed = false;
|
|
97
|
-
constructor(module, document, worker, loadingTask, maxPixels) {
|
|
98
|
+
constructor(module, document, worker, workerPort, loadingTask, maxPixels) {
|
|
98
99
|
this.#module = module;
|
|
99
100
|
this.#document = document;
|
|
100
101
|
this.#worker = worker;
|
|
102
|
+
this.#workerPort = workerPort;
|
|
101
103
|
this.#loadingTask = loadingTask;
|
|
102
104
|
this.#maxPixels = maxPixels;
|
|
103
105
|
this.pageCount = document.numPages;
|
|
@@ -155,7 +157,7 @@ class PdfJsBackend {
|
|
|
155
157
|
const document = await withTimeout(loadingTask.promise, context.limits.maxOperationMs, "PDF parsing");
|
|
156
158
|
if (context.signal.aborted)
|
|
157
159
|
throw abortError();
|
|
158
|
-
return new PdfJsBackend(module, document, worker, loadingTask, context.limits.maxDecodedPixels);
|
|
160
|
+
return new PdfJsBackend(module, document, worker, workerPort, loadingTask, context.limits.maxDecodedPixels);
|
|
159
161
|
}
|
|
160
162
|
catch (error) {
|
|
161
163
|
await loadingTask.destroy().catch(() => undefined);
|
|
@@ -168,9 +170,14 @@ class PdfJsBackend {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
async renderPage(target, pageIndex, zoom, devicePixelRatio, signal) {
|
|
171
|
-
this.#
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
if (this.#closed || signal?.aborted)
|
|
174
|
+
throw abortError();
|
|
175
|
+
const page = await this.#page(pageIndex).catch((error) => {
|
|
176
|
+
if (this.#closed || signal?.aborted)
|
|
177
|
+
throw abortError();
|
|
178
|
+
throw error;
|
|
179
|
+
});
|
|
180
|
+
if (this.#closed || signal?.aborted)
|
|
174
181
|
throw abortError();
|
|
175
182
|
const viewport = page.getViewport({ scale: CSS_UNITS * zoom });
|
|
176
183
|
const dpr = Math.max(1, devicePixelRatio);
|
|
@@ -200,11 +207,12 @@ class PdfJsBackend {
|
|
|
200
207
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
201
208
|
try {
|
|
202
209
|
await renderTask.promise;
|
|
203
|
-
if (signal?.aborted)
|
|
210
|
+
if (this.#closed || signal?.aborted)
|
|
204
211
|
throw abortError();
|
|
205
212
|
}
|
|
206
213
|
catch (error) {
|
|
207
|
-
if (
|
|
214
|
+
if (this.#closed ||
|
|
215
|
+
signal?.aborted ||
|
|
208
216
|
error instanceof this.#module.RenderingCancelledException ||
|
|
209
217
|
(error instanceof Error && error.name === "RenderingCancelledException"))
|
|
210
218
|
throw abortError();
|
|
@@ -251,11 +259,14 @@ class PdfJsBackend {
|
|
|
251
259
|
this.#closed = true;
|
|
252
260
|
this.#pages.clear();
|
|
253
261
|
try {
|
|
254
|
-
|
|
262
|
+
// Destroy cancels and awaits active renders. PDF.js cleanup is only
|
|
263
|
+
// safe while idle, so it must not run before retiring this document.
|
|
255
264
|
await this.#loadingTask.destroy();
|
|
256
265
|
}
|
|
257
266
|
finally {
|
|
258
267
|
this.#worker.destroy();
|
|
268
|
+
// PDFWorker does not terminate an externally supplied port; we own it.
|
|
269
|
+
this.#workerPort.terminate();
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
#page(pageIndex) {
|
package/dist/edit/engine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DocumentFormat, RegisteredFont, ResourceLimits, TextRun, ViewerWarning } from "../contracts.js";
|
|
2
2
|
import type { EditSession } from "./sessions.js";
|
|
3
|
-
import type { ChangeMode, EditableFormat, EditElement, EditFindOptions, EditOperation, EditSessionBase, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, TextTarget } from "./types.js";
|
|
3
|
+
import type { ChangeMode, EditableFormat, EditElement, EditFindOptions, EditOperation, EditSessionBase, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, TextTarget, TextAnchorMigration } from "./types.js";
|
|
4
4
|
export interface EditEngineContext {
|
|
5
5
|
readonly format: EditableFormat;
|
|
6
6
|
readonly fileName?: string;
|
|
@@ -74,6 +74,7 @@ export interface EngineChange {
|
|
|
74
74
|
readonly removedIds: readonly string[];
|
|
75
75
|
/** Old id → new id, when the format had to rename an element. */
|
|
76
76
|
readonly remappedIds?: Readonly<Record<string, string>>;
|
|
77
|
+
readonly textAnchorMigrations?: readonly TextAnchorMigration[];
|
|
77
78
|
/** A superset of the pages whose content changed. */
|
|
78
79
|
readonly changedPages: readonly number[];
|
|
79
80
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EngineBatch, EngineChange, MaterializedDocument, RestoreTarget } from "../../engine.js";
|
|
2
2
|
import type { EditFindOptions, EditOperation, ElementQuery, OperationIssue, PagePoint, PageRect, TextPosition, TextRange, TextTarget } from "../../types.js";
|
|
3
|
-
import type { PageLayout, PdfElement, PdfOperation, TextFont, TextLayout } from "../types.js";
|
|
3
|
+
import type { PageLayout, PdfElement, PdfOperation, PdfTextParagraph, TextFont, TextLayout } from "../types.js";
|
|
4
4
|
import type { EditWorkerBitmap } from "../../../worker-protocol.js";
|
|
5
5
|
import { FontLibrary } from "./fonts.js";
|
|
6
6
|
import { ImageCache } from "./images.js";
|
|
@@ -76,7 +76,9 @@ export declare class PdfEditDocument {
|
|
|
76
76
|
/** Elements under a point, top-most (drawn last) first. */
|
|
77
77
|
elementsAt(pageIndex: number, point: PagePoint): PdfElement[];
|
|
78
78
|
findText(query: string, options: EditFindOptions): TextTarget[];
|
|
79
|
-
/**
|
|
79
|
+
/** Resolves the canonical imported paragraph, when its rows can be grouped safely. */
|
|
80
|
+
textParagraph(elementId: string): PdfTextParagraph | undefined;
|
|
81
|
+
/** Lines, glyph boxes and styles of a text, text box, paragraph or table element. */
|
|
80
82
|
textLayout(elementId: string): TextLayout | undefined;
|
|
81
83
|
/** The browser face of the font a text or text box element is drawn in, see `TextFont`. */
|
|
82
84
|
textFont(elementId: string): TextFont | undefined;
|
|
@@ -7,6 +7,8 @@ import { textFaceOf } from "./text-font.js";
|
|
|
7
7
|
import { fontRequestsOf } from "./text-box.js";
|
|
8
8
|
import { issueCollector, } from "./operations.js";
|
|
9
9
|
import { insertTextBox } from "./text-box.js";
|
|
10
|
+
import { discoverParagraphs, paragraphElement, } from "./paragraph.js";
|
|
11
|
+
import { replaceParagraphText } from "./paragraph-edit.js";
|
|
10
12
|
import { replaceText, setTextStyle } from "./existing-text.js";
|
|
11
13
|
import { deleteElement, moveElement, resizeElement } from "./transform.js";
|
|
12
14
|
import { deletePage, insertPage, movePage, rotatePage } from "./pages.js";
|
|
@@ -90,6 +92,7 @@ function referenceOf(operation) {
|
|
|
90
92
|
return typeof target === "string" ? parseReference(target) : undefined;
|
|
91
93
|
}
|
|
92
94
|
const handlers = {
|
|
95
|
+
replaceParagraphText: replaceParagraphText,
|
|
93
96
|
insertTextBox: insertTextBox,
|
|
94
97
|
replaceText: replaceText,
|
|
95
98
|
setTextStyle: setTextStyle,
|
|
@@ -209,6 +212,7 @@ export class PdfEditDocument {
|
|
|
209
212
|
const createdIds = [];
|
|
210
213
|
const createdByOperation = [];
|
|
211
214
|
const removedIds = [];
|
|
215
|
+
const textAnchorMigrations = [];
|
|
212
216
|
const changedPages = new Set();
|
|
213
217
|
const warnings = [];
|
|
214
218
|
// The first change of a signed, certified, tagged or PDF/A file is the
|
|
@@ -230,6 +234,8 @@ export class PdfEditDocument {
|
|
|
230
234
|
createdIds.push(...result.createdIds);
|
|
231
235
|
createdByOperation[operationIndex] = [...result.createdIds];
|
|
232
236
|
removedIds.push(...(result.removedIds ?? []));
|
|
237
|
+
for (const migration of result.textAnchorMigrations ?? [])
|
|
238
|
+
textAnchorMigrations.push({ ...migration, operationIndex });
|
|
233
239
|
for (const pageIndex of result.changedPages)
|
|
234
240
|
changedPages.add(pageIndex);
|
|
235
241
|
warnings.push(...result.warnings);
|
|
@@ -238,6 +244,7 @@ export class PdfEditDocument {
|
|
|
238
244
|
return {
|
|
239
245
|
createdIds,
|
|
240
246
|
removedIds,
|
|
247
|
+
...(textAnchorMigrations.length ? { textAnchorMigrations } : {}),
|
|
241
248
|
changedPages: [...changedPages].sort((a, b) => a - b),
|
|
242
249
|
pageCount: this.pageCount,
|
|
243
250
|
warnings,
|
|
@@ -368,7 +375,13 @@ export class PdfEditDocument {
|
|
|
368
375
|
const pageIndex = this.#pageIndexOf(id);
|
|
369
376
|
if (pageIndex === undefined)
|
|
370
377
|
return undefined;
|
|
371
|
-
|
|
378
|
+
const element = this.#elementsOf(pageIndex).find((element) => element.id === id);
|
|
379
|
+
if (element)
|
|
380
|
+
return element;
|
|
381
|
+
const target = this.#pages[pageIndex]?.paragraphs?.get(id);
|
|
382
|
+
return target?.paragraph.id === id
|
|
383
|
+
? paragraphElement(target.paragraph)
|
|
384
|
+
: undefined;
|
|
372
385
|
}
|
|
373
386
|
/** Elements under a point, top-most (drawn last) first. */
|
|
374
387
|
elementsAt(pageIndex, point) {
|
|
@@ -395,25 +408,60 @@ export class PdfEditDocument {
|
|
|
395
408
|
});
|
|
396
409
|
return targets;
|
|
397
410
|
}
|
|
398
|
-
/**
|
|
411
|
+
/** Resolves the canonical imported paragraph, when its rows can be grouped safely. */
|
|
412
|
+
textParagraph(elementId) {
|
|
413
|
+
return this.#paragraphOf(elementId)?.paragraph;
|
|
414
|
+
}
|
|
415
|
+
#paragraphOf(id) {
|
|
416
|
+
const pageIndex = this.#pageIndexOf(id);
|
|
417
|
+
if (pageIndex === undefined)
|
|
418
|
+
return undefined;
|
|
419
|
+
this.#elementsOf(pageIndex);
|
|
420
|
+
return this.#pages[pageIndex]?.paragraphs?.get(id);
|
|
421
|
+
}
|
|
422
|
+
/** Lines, glyph boxes and styles of a text, text box, paragraph or table element. */
|
|
399
423
|
textLayout(elementId) {
|
|
400
424
|
const pageIndex = this.#pageIndexOf(elementId);
|
|
401
425
|
if (pageIndex === undefined)
|
|
402
426
|
return undefined;
|
|
403
|
-
|
|
427
|
+
const paragraph = this.#paragraphOf(elementId);
|
|
428
|
+
return this.#scanText(pageIndex, (raw) => {
|
|
429
|
+
const scan = paragraph?.paragraph.id === elementId
|
|
430
|
+
? this.#paragraphScan(raw, paragraph)
|
|
431
|
+
: raw;
|
|
404
432
|
const element = scan.elements.find((entry) => entry.id === elementId);
|
|
405
|
-
|
|
433
|
+
const layout = element
|
|
434
|
+
? layoutOf(this.#pdfium, scan, element)
|
|
435
|
+
: undefined;
|
|
436
|
+
return layout && paragraph?.paragraph.id === elementId
|
|
437
|
+
? {
|
|
438
|
+
...layout,
|
|
439
|
+
lines: [...layout.lines]
|
|
440
|
+
.sort((a, b) => a.range.start.offset - b.range.start.offset)
|
|
441
|
+
.map((line) => ({
|
|
442
|
+
...line,
|
|
443
|
+
fontSize: paragraph.paragraph.textStyle.fontSize,
|
|
444
|
+
})),
|
|
445
|
+
}
|
|
446
|
+
: layout;
|
|
406
447
|
});
|
|
407
448
|
}
|
|
408
449
|
/** The browser face of the font a text or text box element is drawn in, see `TextFont`. */
|
|
409
450
|
textFont(elementId) {
|
|
410
451
|
const location = this.#locate(elementId);
|
|
411
|
-
|
|
412
|
-
|
|
452
|
+
if (!location)
|
|
453
|
+
return undefined;
|
|
454
|
+
// A paragraph not yet edited is not listed among the elements; its id
|
|
455
|
+
// still locates its rows.
|
|
456
|
+
const kind = this.getElement(elementId)?.kind ??
|
|
457
|
+
(this.#paragraphOf(elementId)?.paragraph.id === elementId
|
|
458
|
+
? "paragraph"
|
|
459
|
+
: undefined);
|
|
460
|
+
if (kind !== "text" && kind !== "textBox" && kind !== "paragraph")
|
|
413
461
|
return undefined;
|
|
414
462
|
const { lib } = this.#pdfium;
|
|
415
463
|
return this.#withPage(location.pageIndex, (page) => {
|
|
416
|
-
// A text box's lines share their font;
|
|
464
|
+
// A text box's lines and a paragraph's rows share their font; the first answers.
|
|
417
465
|
const object = location.indexes
|
|
418
466
|
.map((index) => lib.FPDFPage_GetObject(page, index))
|
|
419
467
|
.find((entry) => lib.FPDFPageObj_GetType(entry) === OBJECT_TEXT);
|
|
@@ -465,7 +513,10 @@ export class PdfEditDocument {
|
|
|
465
513
|
if (!Number.isSafeInteger(pixels) ||
|
|
466
514
|
pixels > this.#limits.maxDecodedPixels)
|
|
467
515
|
throw new ViewerError("resource-limit", "The rendered page exceeds maxDecodedPixels", { details: { actual: pixels, limit: this.#limits.maxDecodedPixels } });
|
|
468
|
-
const wanted = new Set(elementIds)
|
|
516
|
+
const wanted = new Set(elementIds.flatMap((id) => {
|
|
517
|
+
const paragraph = this.#paragraphOf(id)?.paragraph;
|
|
518
|
+
return paragraph?.id === id ? paragraph.memberIds : [id];
|
|
519
|
+
}));
|
|
469
520
|
const records = this.#objectsOf(pageIndex, page);
|
|
470
521
|
const suppressed = [];
|
|
471
522
|
records.forEach((record, index) => {
|
|
@@ -529,7 +580,12 @@ export class PdfEditDocument {
|
|
|
529
580
|
if (pageIndex === undefined ||
|
|
530
581
|
pageIndex !== this.#pageIndexOf(range.end.elementId))
|
|
531
582
|
return [];
|
|
532
|
-
|
|
583
|
+
const paragraph = range.start.elementId === range.end.elementId
|
|
584
|
+
? this.#paragraphOf(range.start.elementId)
|
|
585
|
+
: undefined;
|
|
586
|
+
return this.#scanText(pageIndex, (scan) => rectsOf(this.#pdfium, paragraph?.paragraph.id === range.start.elementId
|
|
587
|
+
? this.#paragraphScan(scan, paragraph)
|
|
588
|
+
: scan, range));
|
|
533
589
|
}
|
|
534
590
|
dispose() {
|
|
535
591
|
this.#fonts.release(this.#pdfium, this.#document.handle);
|
|
@@ -561,10 +617,14 @@ export class PdfEditDocument {
|
|
|
561
617
|
// part of the base, so a replay meets them and picks the same id.
|
|
562
618
|
newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
|
|
563
619
|
withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
|
|
620
|
+
readPage: (pageIndex, use) => this.#withPage(pageIndex, use),
|
|
621
|
+
paragraph: (id) => this.#paragraphOf(id),
|
|
622
|
+
pageElements: (pageIndex) => this.#elementsOf(pageIndex),
|
|
564
623
|
appendObjects: (pageIndex, records) => {
|
|
565
624
|
const page = this.#pages[pageIndex];
|
|
566
625
|
page.objects = [...(page.objects ?? []), ...records];
|
|
567
626
|
delete page.elements;
|
|
627
|
+
delete page.paragraphs;
|
|
568
628
|
},
|
|
569
629
|
locate: (id) => this.#locate(id),
|
|
570
630
|
element: (id) => this.getElement(id),
|
|
@@ -591,6 +651,7 @@ export class PdfEditDocument {
|
|
|
591
651
|
return;
|
|
592
652
|
delete page.geometry;
|
|
593
653
|
delete page.elements;
|
|
654
|
+
delete page.paragraphs;
|
|
594
655
|
},
|
|
595
656
|
spliceObjects: (pageIndex, start, count, records) => {
|
|
596
657
|
const page = this.#pages[pageIndex];
|
|
@@ -598,13 +659,16 @@ export class PdfEditDocument {
|
|
|
598
659
|
objects.splice(start, count, ...records);
|
|
599
660
|
page.objects = objects;
|
|
600
661
|
delete page.elements;
|
|
662
|
+
delete page.paragraphs;
|
|
601
663
|
},
|
|
602
664
|
};
|
|
603
665
|
}
|
|
604
666
|
/** Cached elements carry page indexes, which a structure change makes stale. */
|
|
605
667
|
#forgetElements() {
|
|
606
|
-
for (const page of this.#pages)
|
|
668
|
+
for (const page of this.#pages) {
|
|
607
669
|
delete page.elements;
|
|
670
|
+
delete page.paragraphs;
|
|
671
|
+
}
|
|
608
672
|
}
|
|
609
673
|
#locate(id) {
|
|
610
674
|
const pageIndex = this.#pageIndexOf(id);
|
|
@@ -617,7 +681,17 @@ export class PdfEditDocument {
|
|
|
617
681
|
indexes.push(index);
|
|
618
682
|
});
|
|
619
683
|
const record = records[indexes[0] ?? -1];
|
|
620
|
-
|
|
684
|
+
if (record)
|
|
685
|
+
return { pageIndex, indexes, record };
|
|
686
|
+
const paragraph = this.#paragraphOf(id);
|
|
687
|
+
const first = paragraph && records[paragraph.indexes[0] ?? -1];
|
|
688
|
+
return paragraph?.paragraph.id === id && first
|
|
689
|
+
? {
|
|
690
|
+
pageIndex,
|
|
691
|
+
indexes: paragraph.indexes,
|
|
692
|
+
record: { ...first, id, mark: paragraph.spec },
|
|
693
|
+
}
|
|
694
|
+
: undefined;
|
|
621
695
|
}
|
|
622
696
|
/** Loads a page, lets `use` change it, regenerates its content stream. */
|
|
623
697
|
#writePage(pageIndex, use) {
|
|
@@ -636,6 +710,7 @@ export class PdfEditDocument {
|
|
|
636
710
|
details: { stage: "apply", pageIndex },
|
|
637
711
|
});
|
|
638
712
|
delete this.#pages[pageIndex].elements;
|
|
713
|
+
delete this.#pages[pageIndex].paragraphs;
|
|
639
714
|
return result;
|
|
640
715
|
}
|
|
641
716
|
finally {
|
|
@@ -657,7 +732,13 @@ export class PdfEditDocument {
|
|
|
657
732
|
return record.elements;
|
|
658
733
|
return this.#withPage(pageIndex, (page, textPage, geometry) => {
|
|
659
734
|
const { elements } = scanPage(this.#pdfium, page, textPage, pageIndex, geometry, this.#objectsOf(pageIndex, page));
|
|
660
|
-
record.
|
|
735
|
+
record.paragraphs = discoverParagraphs(this.#pdfium, page, pageIndex, geometry, this.#objectsOf(pageIndex, page), elements, textPage, this.#measurer);
|
|
736
|
+
record.elements = Object.freeze(elements.map((element) => {
|
|
737
|
+
const paragraph = record.paragraphs?.get(element.id)?.paragraph;
|
|
738
|
+
return paragraph && paragraph.id !== element.id
|
|
739
|
+
? { ...element, textEditingTarget: paragraph.id }
|
|
740
|
+
: element;
|
|
741
|
+
}));
|
|
661
742
|
return record.elements;
|
|
662
743
|
});
|
|
663
744
|
}
|
|
@@ -779,6 +860,28 @@ export class PdfEditDocument {
|
|
|
779
860
|
};
|
|
780
861
|
return record.geometry;
|
|
781
862
|
}
|
|
863
|
+
#paragraphScan(scan, target) {
|
|
864
|
+
const paragraph = target.paragraph;
|
|
865
|
+
if (paragraph.memberIds.includes(paragraph.id))
|
|
866
|
+
return scan;
|
|
867
|
+
const members = new Map(paragraph.members.map((member) => [member.elementId, member]));
|
|
868
|
+
return {
|
|
869
|
+
...scan,
|
|
870
|
+
elements: [
|
|
871
|
+
...scan.elements.filter((element) => !members.has(element.id)),
|
|
872
|
+
paragraphElement(paragraph),
|
|
873
|
+
],
|
|
874
|
+
offsets: scan.offsets.map((position) => {
|
|
875
|
+
const member = position && members.get(position.elementId);
|
|
876
|
+
return member && position
|
|
877
|
+
? {
|
|
878
|
+
elementId: paragraph.id,
|
|
879
|
+
offset: Math.min(member.end, member.start + position.offset),
|
|
880
|
+
}
|
|
881
|
+
: position;
|
|
882
|
+
}),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
782
885
|
/** Loads a page with its text page and the character-to-element mapping. */
|
|
783
886
|
#scanText(pageIndex, use) {
|
|
784
887
|
return this.#withPage(pageIndex, (page, textPage, geometry) => {
|
|
@@ -2,6 +2,7 @@ import type { PageRect } from "../../types.js";
|
|
|
2
2
|
import type { PdfElement, PdfTextStyle } from "../types.js";
|
|
3
3
|
import { type PageGeometry } from "./geometry.js";
|
|
4
4
|
import type { Pdfium } from "./pdfium.js";
|
|
5
|
+
import type { ParagraphSpec } from "./paragraph.js";
|
|
5
6
|
/** FPDFPageObj_GetType values. */
|
|
6
7
|
export declare const OBJECT_TEXT = 1;
|
|
7
8
|
export declare const OBJECT_PATH = 2;
|
|
@@ -11,10 +12,13 @@ export declare const MARK_NAME = "WebDoc";
|
|
|
11
12
|
export declare const MARK_PARAM = "webdoc";
|
|
12
13
|
/** What a `WebDoc` mark says about the objects it tags. */
|
|
13
14
|
export interface MarkParams {
|
|
14
|
-
readonly kind: "textBox" | "table";
|
|
15
|
+
readonly kind: "textBox" | "table" | "paragraph";
|
|
15
16
|
readonly id: string;
|
|
16
17
|
readonly [key: string]: unknown;
|
|
17
18
|
}
|
|
19
|
+
export declare function paragraphSpecOf(mark: unknown): ParagraphSpec | undefined;
|
|
20
|
+
/** Paragraph marks store the upright frame; the page may subsequently rotate. */
|
|
21
|
+
export declare function paragraphBounds(spec: ParagraphSpec, geometry: PageGeometry): PageRect;
|
|
18
22
|
/** One PDFium page object with what the model knows about it. */
|
|
19
23
|
export interface ObjectRecord {
|
|
20
24
|
/** Element id of a plain object; objects of a marked group share their group id. */
|
|
@@ -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,
|