@embedpdf/plugin-redaction 3.0.0-next.13 → 3.0.0-next.15
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/README.md +66 -24
- package/dist/contract-Ib0iOWF3.cjs +43 -0
- package/dist/contract-Ib0iOWF3.cjs.map +1 -0
- package/dist/contract-TU9orxrO.d.cts +184 -0
- package/dist/contract-TU9orxrO.d.ts +184 -0
- package/dist/contract-eQ3P0UU-.js +33 -0
- package/dist/contract-eQ3P0UU-.js.map +1 -0
- package/dist/contract.cjs +2 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/index.cjs +455 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.js +455 -192
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/contract--fJOCVRN.d.cts +0 -84
- package/dist/contract--fJOCVRN.d.ts +0 -84
- package/dist/contract-BZLrD3p2.js +0 -7
- package/dist/contract-BZLrD3p2.js.map +0 -1
- package/dist/contract-CyLm5eeQ.cjs +0 -11
- package/dist/contract-CyLm5eeQ.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,163 +1,392 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_contract = require("./contract-
|
|
2
|
+
const require_contract = require("./contract-Ib0iOWF3.cjs");
|
|
3
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
4
4
|
let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
|
|
5
|
-
let
|
|
5
|
+
let _embedpdf_plugin_search_contract = require("@embedpdf/plugin-search/contract");
|
|
6
6
|
let _embedpdf_plugin_selection_contract = require("@embedpdf/plugin-selection/contract");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
8
|
+
let _embedpdf_engine_core = require("@embedpdf/engine-core");
|
|
9
|
+
//#region src/read/pending.ts
|
|
10
|
+
/**
|
|
11
|
+
* The pending view: `redact` annotations projected as marks, per page and
|
|
12
|
+
* memoized on the annotation plane's own reference-stable lists, plus the
|
|
13
|
+
* client-side collateral estimate.
|
|
14
|
+
*/
|
|
15
|
+
/** The regions a redact mark targets: its quads' boxes, else its `rect`. */
|
|
16
|
+
const regionsOf = (dto) => dto.quadPoints.length === 0 ? [dto.rect] : dto.quadPoints.map(_embedpdf_engine_core.pageQuadBounds);
|
|
17
|
+
const EMPTY = [];
|
|
18
|
+
/** A `redact` annotation as a mark; `pageIndex` is its page's display index. */
|
|
19
|
+
const markOf = (record, pageIndex) => ({
|
|
20
|
+
ref: record.ref,
|
|
21
|
+
page: record.ref.page,
|
|
22
|
+
pageIndex,
|
|
23
|
+
kind: record.quadPoints.length > 0 ? "text" : "area",
|
|
24
|
+
bounds: record.rect,
|
|
25
|
+
overlayText: record.overlayText ?? null,
|
|
26
|
+
repeat: record.repeat ?? false
|
|
27
|
+
});
|
|
28
|
+
function createPendingReads(ctx, { store, siblings }) {
|
|
29
|
+
const { pages, pageIndexOf } = store;
|
|
30
|
+
const { annotation } = siblings;
|
|
31
|
+
/** One page's marks, the same array while the plane's list and the page's position hold. */
|
|
32
|
+
const marksOn = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => {
|
|
33
|
+
const page = (0, _embedpdf_core.toPageRef)(pageObjectNumber);
|
|
34
|
+
return [annotation.list({ pages: [page] }), pageIndexOf(page)];
|
|
35
|
+
}, (_pageObjectNumber, records, pageIndex) => {
|
|
36
|
+
const marks = records.flatMap((record) => record.subtype === "redact" ? [markOf(record, pageIndex)] : []);
|
|
37
|
+
return marks.length ? marks : EMPTY;
|
|
38
|
+
});
|
|
39
|
+
/** The whole document's marks, the same array while no page's marks changed. */
|
|
40
|
+
const allMarks = (0, _embedpdf_core.memo)(() => pages().map((page) => marksOn(page.objectNumber)), (...perPage) => perPage.some((marks) => marks.length) ? perPage.flat() : EMPTY);
|
|
41
|
+
/** One page's marks (a ref or an index; none for a page that isn't there), or every mark. */
|
|
42
|
+
const listPending = (filter) => {
|
|
43
|
+
if (filter?.page === void 0) return allMarks();
|
|
44
|
+
const layout = ctx.getPage(filter.page);
|
|
45
|
+
return layout ? marksOn(layout.ref.objectNumber) : EMPTY;
|
|
46
|
+
};
|
|
47
|
+
/** A record the annotation plugin made or changed, as the mark it is. */
|
|
48
|
+
const markFor = (record) => record.subtype === "redact" ? markOf(record, pageIndexOf(record.ref.page)) : null;
|
|
49
|
+
const getPending = (ref) => {
|
|
50
|
+
const key = (0, _embedpdf_core.annotationKey)(ref);
|
|
51
|
+
return listPending().find((mark) => (0, _embedpdf_core.annotationKey)(mark.ref) === key) ?? null;
|
|
52
|
+
};
|
|
53
|
+
const estimateCollateral = (refs) => {
|
|
54
|
+
const wanted = refs ? new Set(refs.map(_embedpdf_core.annotationKey)) : null;
|
|
55
|
+
const hits = [];
|
|
56
|
+
for (const page of pages()) {
|
|
57
|
+
const onPage = annotation.list({ pages: [page] });
|
|
58
|
+
const marks = onPage.filter((dto) => dto.subtype === "redact" && (!wanted || wanted.has((0, _embedpdf_core.annotationKey)(dto.ref))));
|
|
59
|
+
if (marks.length === 0) continue;
|
|
60
|
+
const regions = marks.flatMap(regionsOf);
|
|
61
|
+
for (const other of onPage) {
|
|
62
|
+
if (other.subtype === "redact") continue;
|
|
63
|
+
if (regions.some((region) => (0, _embedpdf_core_geometry.rectsOverlap)(region, other.rect))) hits.push(other.ref);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
count: hits.length,
|
|
68
|
+
refs: hits
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
listPending,
|
|
73
|
+
markFor,
|
|
74
|
+
api: {
|
|
75
|
+
listPending,
|
|
76
|
+
getPending,
|
|
77
|
+
getPendingCount: (page) => listPending(page === void 0 ? void 0 : { page }).length,
|
|
78
|
+
estimateCollateral
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/services/events.ts
|
|
84
|
+
function createEvents(ctx) {
|
|
85
|
+
return {
|
|
86
|
+
applied: ctx.events.source(),
|
|
87
|
+
pendingChanged: ctx.events.source()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/services/siblings.ts
|
|
92
|
+
/** The sibling plugins: annotation (required, host lens), selection and search (optional). */
|
|
93
|
+
function resolveSiblings(ctx) {
|
|
94
|
+
return {
|
|
95
|
+
annotation: ctx.get(_embedpdf_plugin_annotation_contract_host.AnnotationToken),
|
|
96
|
+
selection: () => ctx.tryGet(_embedpdf_plugin_selection_contract.SelectionToken),
|
|
97
|
+
search: () => ctx.tryGet(_embedpdf_plugin_search_contract.SearchToken)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/services/store.ts
|
|
102
|
+
/** The page registry reads and the engine's redaction service. */
|
|
103
|
+
/** Applying destroys content: its own granted power, narrower than annotate.
|
|
104
|
+
* The engine's apply asserts all three (the local redaction service and the
|
|
105
|
+
* cloud route guards alike): the redact grant itself, the page-content
|
|
11
106
|
* rewrite, and the annotation consumption. `canApply` mirrors the full set. */
|
|
12
107
|
const APPLY_CAPABILITIES = [
|
|
13
108
|
"doc.redact",
|
|
14
109
|
"doc.pages.modify",
|
|
15
110
|
"doc.annotate.modify"
|
|
16
111
|
];
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
left: Math.min(...xs),
|
|
37
|
-
right: Math.max(...xs),
|
|
38
|
-
bottom: Math.min(...ys),
|
|
39
|
-
top: Math.max(...ys)
|
|
40
|
-
};
|
|
41
|
-
});
|
|
112
|
+
function createStore(ctx) {
|
|
113
|
+
const requireService = () => {
|
|
114
|
+
const service = ctx.doc.redaction;
|
|
115
|
+
if (!service) throw new _embedpdf_core.PluginError("unsupported", "redaction", "this engine has no redaction service");
|
|
116
|
+
return service;
|
|
117
|
+
};
|
|
118
|
+
/** Every page's ref, in display order. */
|
|
119
|
+
const pages = () => (ctx.document()?.pages ?? []).map((page) => page.ref);
|
|
120
|
+
/** Page object number → display index, rebuilt only when the page registry changes. */
|
|
121
|
+
const indexByPage = (0, _embedpdf_core.memo)(() => [ctx.document()?.pages], (layouts) => new Map((layouts ?? []).map((page) => [page.ref.objectNumber, page.index])));
|
|
122
|
+
const pageIndexOf = (page) => indexByPage().get(page.objectNumber) ?? -1;
|
|
123
|
+
const canApply = () => ctx.doc.redaction !== void 0 && APPLY_CAPABILITIES.every((capability) => ctx.doc.security.allows(capability));
|
|
124
|
+
return {
|
|
125
|
+
requireService,
|
|
126
|
+
pages,
|
|
127
|
+
pageIndexOf,
|
|
128
|
+
canApply
|
|
129
|
+
};
|
|
42
130
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!doc) throw new Error("[redaction] no document bound");
|
|
51
|
-
return doc;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/services/index.ts
|
|
133
|
+
function createServices(ctx) {
|
|
134
|
+
return {
|
|
135
|
+
events: createEvents(ctx),
|
|
136
|
+
store: createStore(ctx),
|
|
137
|
+
siblings: resolveSiblings(ctx)
|
|
52
138
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/model.ts
|
|
142
|
+
const initialRedactionState = () => ({
|
|
143
|
+
applying: false,
|
|
144
|
+
lastResult: null
|
|
145
|
+
});
|
|
146
|
+
const startApply = (state) => state.applying ? state : {
|
|
147
|
+
...state,
|
|
148
|
+
applying: true
|
|
149
|
+
};
|
|
150
|
+
const finishApply = (state) => state.applying ? {
|
|
151
|
+
...state,
|
|
152
|
+
applying: false
|
|
153
|
+
} : state;
|
|
154
|
+
const setLastResult = (state, result) => state.lastResult === result ? state : {
|
|
155
|
+
...state,
|
|
156
|
+
lastResult: result
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/sync/document-events.ts
|
|
160
|
+
function subscribeChanges(ctx, { events, siblings }) {
|
|
161
|
+
const { applied, pendingChanged } = events;
|
|
162
|
+
const { annotation } = siblings;
|
|
163
|
+
ctx.listen(ctx.doc.events, (event) => {
|
|
164
|
+
if (event.type !== "redaction.applied") return;
|
|
165
|
+
const { type: _type, origin, ...result } = event;
|
|
166
|
+
ctx.state.update(setLastResult, result);
|
|
167
|
+
applied.emit({
|
|
168
|
+
result,
|
|
169
|
+
origin
|
|
70
170
|
});
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const cb of appliedCallbacks) try {
|
|
75
|
-
cb(result);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error("[redaction] onApplied callback failed", err);
|
|
78
|
-
}
|
|
171
|
+
});
|
|
172
|
+
const markChanged = ({ annotation: changed }) => {
|
|
173
|
+
if (changed.subtype === "redact") pendingChanged.emit({ pages: [changed.page] });
|
|
79
174
|
};
|
|
80
|
-
|
|
175
|
+
ctx.listen(annotation.onCreated, markChanged);
|
|
176
|
+
ctx.listen(annotation.onUpdated, markChanged);
|
|
177
|
+
ctx.listen(annotation.onDeleted, (event) => pendingChanged.emit({ pages: [event.page] }));
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/sync/overlay.ts
|
|
181
|
+
/** The redact tool's fields the overlay settings stand for. */
|
|
182
|
+
const toolDefaultsOf = ({ overlay }) => ({
|
|
183
|
+
interiorColor: overlay.fill,
|
|
184
|
+
fontColor: overlay.text.color,
|
|
185
|
+
fontFamily: overlay.text.fontFamily,
|
|
186
|
+
fontSize: overlay.text.fontSize
|
|
187
|
+
});
|
|
188
|
+
function syncOverlay(ctx, { siblings }) {
|
|
189
|
+
const { annotation } = siblings;
|
|
190
|
+
const settings = ctx.settings();
|
|
191
|
+
const write = () => annotation.tools.updateDefaults("redact", toolDefaultsOf(settings.get()));
|
|
192
|
+
write();
|
|
193
|
+
ctx.listen(settings.api.onSettingsChanged, ({ changed }) => {
|
|
194
|
+
if (changed.includes("overlay")) write();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/write/apply.ts
|
|
199
|
+
/**
|
|
200
|
+
* The destructive half: `doc.redaction.apply` in refs or pages scope. Applies
|
|
201
|
+
* run one at a time, in call order, so each one sees the marks the previous
|
|
202
|
+
* one left. The result reaches `lastResult` and `onApplied` through the
|
|
203
|
+
* confirmed `redaction.applied` event, which the engine publishes before the
|
|
204
|
+
* apply resolves; this area only brackets the call with the `applying` flag.
|
|
205
|
+
*/
|
|
206
|
+
function createApplying(ctx, { store }, { listPending }) {
|
|
207
|
+
const { requireService, pages } = store;
|
|
208
|
+
const queue = ctx.serialQueue("apply");
|
|
209
|
+
ctx.onSettle(() => queue.idle());
|
|
210
|
+
/**
|
|
211
|
+
* Queue an apply whose scope is computed when its turn comes. One whose
|
|
212
|
+
* signal fired while it waited never starts; one cancelled while the
|
|
213
|
+
* engine applies rejects `operation-cancelled` at once, and the engine's
|
|
214
|
+
* `redaction.applied` says what it removed.
|
|
215
|
+
*/
|
|
216
|
+
const runApply = (scopeOf, options) => queue(async () => {
|
|
81
217
|
const service = requireService();
|
|
82
|
-
|
|
218
|
+
const scope = scopeOf();
|
|
219
|
+
ctx.state.update(startApply);
|
|
83
220
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
result
|
|
88
|
-
});
|
|
89
|
-
fireApplied(result);
|
|
90
|
-
return result;
|
|
91
|
-
} catch (err) {
|
|
92
|
-
ctx.dispatch({
|
|
93
|
-
type: "APPLY_FINISHED",
|
|
94
|
-
result: null
|
|
95
|
-
});
|
|
96
|
-
throw err;
|
|
221
|
+
return await ctx.cancellable(options?.signal, service.apply(scope));
|
|
222
|
+
} finally {
|
|
223
|
+
ctx.state.update(finishApply);
|
|
97
224
|
}
|
|
225
|
+
}, { signal: options?.signal });
|
|
226
|
+
const apply = (refs, options) => runApply(() => {
|
|
227
|
+
const wanted = new Set(refs.map(_embedpdf_core.annotationKey));
|
|
228
|
+
const marks = listPending().filter((mark) => wanted.has((0, _embedpdf_core.annotationKey)(mark.ref))).map((mark) => mark.ref);
|
|
229
|
+
if (marks.length === 0) throw new _embedpdf_core.PluginError("not-found", "redaction", "no matching pending marks");
|
|
230
|
+
return { annotations: marks };
|
|
231
|
+
}, options);
|
|
232
|
+
const applyPages = async (targets, options) => {
|
|
233
|
+
if (targets.length === 0) throw new _embedpdf_core.PluginError("invalid-input", "redaction", "no pages given");
|
|
234
|
+
const pages = targets.map((page) => ctx.pageOf(page).ref);
|
|
235
|
+
return runApply(() => ({ pages }), options);
|
|
98
236
|
};
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
237
|
+
const applyAll = (options) => runApply(() => {
|
|
238
|
+
const all = pages();
|
|
239
|
+
if (all.length === 0) throw new _embedpdf_core.PluginError("not-ready", "redaction", "the document has no pages");
|
|
240
|
+
return { pages: all };
|
|
241
|
+
}, options);
|
|
242
|
+
return { api: {
|
|
243
|
+
apply,
|
|
244
|
+
applyPages,
|
|
245
|
+
applyAll
|
|
246
|
+
} };
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/write/marks.ts
|
|
250
|
+
/**
|
|
251
|
+
* Marking: every mark is a `redact` annotation created through the annotation
|
|
252
|
+
* plugin, in the style of the redact tool's defaults, which follow the
|
|
253
|
+
* `overlay` settings (`sync/overlay.ts`). Removing a mark and changing its
|
|
254
|
+
* label are annotation deletes and updates, checked per mark.
|
|
255
|
+
*/
|
|
256
|
+
/** Stop before the next step once the caller cancelled. */
|
|
257
|
+
const throwIfCancelled = (signal) => {
|
|
258
|
+
if (signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "redaction", "operation cancelled", { cause: signal.reason });
|
|
259
|
+
};
|
|
260
|
+
function createMarking(ctx, { siblings }, { listPending, markFor }) {
|
|
261
|
+
const { annotation, selection, search } = siblings;
|
|
262
|
+
/**
|
|
263
|
+
* A new mark's style, as the engine takes it: the redact tool's current
|
|
264
|
+
* defaults, which carry the `overlay` settings, so a mark from code looks
|
|
265
|
+
* like one drawn with the tool.
|
|
266
|
+
*/
|
|
267
|
+
const markStyle = () => {
|
|
268
|
+
const tool = annotation.tools.getDefaults("redact");
|
|
269
|
+
return {
|
|
270
|
+
color: tool.color,
|
|
271
|
+
interiorColor: tool.interiorColor,
|
|
272
|
+
opacity: tool.opacity,
|
|
273
|
+
fontFamily: tool.fontFamily,
|
|
274
|
+
fontSize: tool.fontSize,
|
|
275
|
+
fontColor: tool.fontColor,
|
|
276
|
+
textAlign: tool.textAlign
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
/** The mark a created or changed record is. */
|
|
280
|
+
const markOfRecord = (record) => {
|
|
281
|
+
const mark = markFor(record);
|
|
282
|
+
if (!mark) throw new _embedpdf_core.PluginError("operation-failed", "redaction", "the record is not a mark");
|
|
283
|
+
return mark;
|
|
284
|
+
};
|
|
285
|
+
/** Whether a ref is a mark waiting to be applied. */
|
|
286
|
+
const isPending = (ref) => {
|
|
287
|
+
const key = (0, _embedpdf_core.annotationKey)(ref);
|
|
288
|
+
return listPending().some((mark) => (0, _embedpdf_core.annotationKey)(mark.ref) === key);
|
|
289
|
+
};
|
|
290
|
+
const canUnmark = (ref) => isPending(ref) && annotation.canDelete(ref);
|
|
291
|
+
const canUpdateLabel = (ref) => isPending(ref) && annotation.canUpdate(ref);
|
|
292
|
+
const markSelection = async (options) => {
|
|
293
|
+
ctx.assertAllowed("annotations:create", "redaction.markSelection");
|
|
294
|
+
const plane = selection();
|
|
295
|
+
if (!plane) throw new _embedpdf_core.PluginError("unsupported", "redaction", "no selection plugin");
|
|
296
|
+
if (!plane.hasSelection()) return { marks: [] };
|
|
297
|
+
const { annotations } = await annotation.createFromSelection("redact", {
|
|
298
|
+
clear: true,
|
|
299
|
+
signal: options?.signal
|
|
106
300
|
});
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
301
|
+
return { marks: annotations.map(markOfRecord) };
|
|
302
|
+
};
|
|
303
|
+
const createMark = async (page, region, signal) => {
|
|
304
|
+
const created = await annotation.create(page, {
|
|
305
|
+
subtype: "redact",
|
|
306
|
+
...region,
|
|
307
|
+
...markStyle()
|
|
308
|
+
}, void 0, { signal });
|
|
309
|
+
return markOfRecord(created.annotation);
|
|
310
|
+
};
|
|
311
|
+
const markArea = async (page, bounds, options) => {
|
|
312
|
+
ctx.assertAllowed("annotations:create", "redaction.markArea");
|
|
313
|
+
const target = ctx.pageOf(page).ref;
|
|
314
|
+
return { mark: await createMark(target, { rect: bounds }, options?.signal) };
|
|
315
|
+
};
|
|
316
|
+
const markPage = async (page, options) => {
|
|
317
|
+
ctx.assertAllowed("annotations:create", "redaction.markPage");
|
|
318
|
+
const layout = ctx.pageOf(page);
|
|
319
|
+
const whole = {
|
|
320
|
+
x: 0,
|
|
321
|
+
y: 0,
|
|
322
|
+
width: layout.size.width,
|
|
323
|
+
height: layout.size.height
|
|
324
|
+
};
|
|
325
|
+
return { mark: await createMark(layout.ref, { rect: whole }, options?.signal) };
|
|
326
|
+
};
|
|
327
|
+
const markMatches = async (query, options) => {
|
|
328
|
+
ctx.assertAllowed("annotations:create", "redaction.markMatches");
|
|
329
|
+
const signal = options?.signal;
|
|
330
|
+
const finder = search();
|
|
331
|
+
if (!finder) throw new _embedpdf_core.PluginError("unsupported", "redaction", "no search plugin");
|
|
332
|
+
const wanted = options?.pages ? new Set(options.pages.map((page) => ctx.pageOf(page).ref.objectNumber)) : null;
|
|
333
|
+
await finder.search(query, { signal });
|
|
334
|
+
const marks = [];
|
|
335
|
+
for (const hit of finder.listHits()) {
|
|
336
|
+
if (wanted && !wanted.has(hit.page.objectNumber)) continue;
|
|
337
|
+
if (hit.segments.length === 0) continue;
|
|
338
|
+
throwIfCancelled(signal);
|
|
339
|
+
marks.push(await createMark(hit.page, { quadPoints: hit.segments.map((segment) => segment.quad) }, signal));
|
|
340
|
+
}
|
|
341
|
+
return { marks };
|
|
342
|
+
};
|
|
343
|
+
const unmark = async (refs, options) => {
|
|
344
|
+
const signal = options?.signal;
|
|
345
|
+
throwIfCancelled(signal);
|
|
346
|
+
const applied = [];
|
|
347
|
+
const skipped = [];
|
|
348
|
+
const failed = [];
|
|
349
|
+
for (const ref of refs) {
|
|
350
|
+
if (!isPending(ref)) {
|
|
351
|
+
skipped.push({
|
|
352
|
+
ref,
|
|
353
|
+
reason: "not a pending redaction mark"
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!annotation.canDelete(ref)) {
|
|
358
|
+
const refused = new _embedpdf_core.PluginError("permission-denied", "redaction", "redaction.unmark requires annotations:delete", { permission: "annotations:delete" });
|
|
359
|
+
failed.push({
|
|
360
|
+
ref,
|
|
361
|
+
error: (0, _embedpdf_core.toPluginErrorInfo)(refused)
|
|
362
|
+
});
|
|
363
|
+
continue;
|
|
133
364
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
let count = 0;
|
|
145
|
-
for (const pon of pons()) {
|
|
146
|
-
const all = anno.list(pon);
|
|
147
|
-
const marks = all.filter((a) => a.subtype === "redact" && (!wanted || wanted.has(idOf(a.ref))));
|
|
148
|
-
if (marks.length === 0) continue;
|
|
149
|
-
const regions = marks.flatMap(regionsOf);
|
|
150
|
-
for (const other of all) {
|
|
151
|
-
if (other.subtype === "redact") continue;
|
|
152
|
-
if (regions.some((r) => intersects(r, other.rect))) count++;
|
|
153
|
-
}
|
|
365
|
+
try {
|
|
366
|
+
await annotation.delete(ref, { signal });
|
|
367
|
+
applied.push(ref);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const refused = (0, _embedpdf_core.toPluginError)("redaction", error);
|
|
370
|
+
if (refused.code === "operation-cancelled") throw refused;
|
|
371
|
+
failed.push({
|
|
372
|
+
ref,
|
|
373
|
+
error: (0, _embedpdf_core.toPluginErrorInfo)(refused)
|
|
374
|
+
});
|
|
154
375
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
applied,
|
|
379
|
+
skipped,
|
|
380
|
+
failed
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
const clearPending = (options) => unmark(listPending().map((mark) => mark.ref), options);
|
|
384
|
+
const updateLabel = async (ref, patch, options) => {
|
|
385
|
+
const current = annotation.get(ref);
|
|
386
|
+
if (!current || current.subtype !== "redact") throw new _embedpdf_core.PluginError("not-found", "redaction", "the target is not a redaction mark");
|
|
387
|
+
if (!annotation.canUpdate(ref)) throw new _embedpdf_core.PluginError("permission-denied", "redaction", "redaction.updateLabel requires annotations:update", { permission: "annotations:update" });
|
|
388
|
+
try {
|
|
389
|
+
const updated = await annotation.update(ref, {
|
|
161
390
|
subtype: "redact",
|
|
162
391
|
...patch.overlayText !== void 0 ? { overlayText: patch.overlayText } : {},
|
|
163
392
|
...patch.repeat !== void 0 ? { repeat: patch.repeat } : {},
|
|
@@ -165,74 +394,110 @@ function createRedactionCapability(ctx) {
|
|
|
165
394
|
fontSize: current.fontSize,
|
|
166
395
|
fontColor: current.fontColor,
|
|
167
396
|
textAlign: current.textAlign
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const refs = collectPending().filter((p) => wanted.has(p.id)).map((p) => p.ref);
|
|
173
|
-
if (refs.length === 0) throw new Error("[redaction] no matching pending redactions");
|
|
174
|
-
return runApply({
|
|
175
|
-
kind: "annotations",
|
|
176
|
-
refs
|
|
177
|
-
});
|
|
178
|
-
},
|
|
179
|
-
applyAll: async () => {
|
|
180
|
-
const pageObjectNumbers = pons();
|
|
181
|
-
if (pageObjectNumbers.length === 0) throw new Error("[redaction] document has no pages");
|
|
182
|
-
return runApply({
|
|
183
|
-
kind: "pages",
|
|
184
|
-
pageObjectNumbers
|
|
185
|
-
});
|
|
186
|
-
},
|
|
187
|
-
onApplied: (cb) => {
|
|
188
|
-
appliedCallbacks.add(cb);
|
|
189
|
-
return () => appliedCallbacks.delete(cb);
|
|
397
|
+
}, void 0, { signal: options?.signal });
|
|
398
|
+
return { mark: markOfRecord(updated.annotation) };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw (0, _embedpdf_core.toPluginError)("redaction", error);
|
|
190
401
|
}
|
|
191
402
|
};
|
|
403
|
+
return { api: {
|
|
404
|
+
canUnmark,
|
|
405
|
+
canUpdateLabel,
|
|
406
|
+
markSelection,
|
|
407
|
+
markArea,
|
|
408
|
+
markPage,
|
|
409
|
+
markMatches,
|
|
410
|
+
unmark,
|
|
411
|
+
clearPending,
|
|
412
|
+
updateLabel
|
|
413
|
+
} };
|
|
192
414
|
}
|
|
193
415
|
//#endregion
|
|
194
|
-
//#region src/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
416
|
+
//#region src/controller.ts
|
|
417
|
+
/**
|
|
418
|
+
* The redaction controller: the composition root. It builds the plugin's
|
|
419
|
+
* services once, wires each area with the services it declares, and
|
|
420
|
+
* assembles the capability from the areas' API slices. No behavior lives
|
|
421
|
+
* here: every verb and read has a home in `read/`, `write/` or `sync/`.
|
|
422
|
+
*/
|
|
423
|
+
function createRedactionController(ctx) {
|
|
424
|
+
const services = createServices(ctx);
|
|
425
|
+
const { events, store } = services;
|
|
426
|
+
const settings = ctx.settings();
|
|
427
|
+
const pending = createPendingReads(ctx, services);
|
|
428
|
+
const marking = createMarking(ctx, services, pending);
|
|
429
|
+
const applying = createApplying(ctx, services, pending);
|
|
430
|
+
return {
|
|
431
|
+
api: (0, _embedpdf_core.composeApi)("redaction", [
|
|
432
|
+
settings.api,
|
|
433
|
+
pending.api,
|
|
434
|
+
marking.api,
|
|
435
|
+
applying.api,
|
|
436
|
+
{
|
|
437
|
+
canMark: () => ctx.allows("annotations:create"),
|
|
438
|
+
canApply: store.canApply,
|
|
439
|
+
isApplying: () => ctx.state.get().applying,
|
|
440
|
+
getLastResult: () => ctx.state.get().lastResult,
|
|
441
|
+
onApplied: events.applied.on,
|
|
442
|
+
onPendingChanged: events.pendingChanged.on
|
|
443
|
+
}
|
|
444
|
+
]),
|
|
445
|
+
connect() {
|
|
446
|
+
subscribeChanges(ctx, services);
|
|
447
|
+
syncOverlay(ctx, services);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
212
451
|
//#endregion
|
|
213
452
|
//#region src/redaction.plugin.ts
|
|
214
453
|
/**
|
|
215
|
-
* Document-scoped redaction plugin
|
|
216
|
-
* model. Marking is annotation
|
|
217
|
-
*
|
|
218
|
-
*
|
|
454
|
+
* Document-scoped redaction plugin: the destructive half of the two-stage
|
|
455
|
+
* model. Marking is annotation work (the composed `redact` tool ships with
|
|
456
|
+
* plugin-annotation); this plugin wraps `doc.redaction.apply`, projects the
|
|
457
|
+
* pending queue out of annotation state, and estimates collateral for
|
|
219
458
|
* confirm dialogs. It owns no mark state of its own.
|
|
220
459
|
*
|
|
221
|
-
* Trust boundary: on a layered document, applying rewrites the
|
|
222
|
-
* the immutable base keeps the original.
|
|
460
|
+
* Trust boundary: on a layered document, applying rewrites the layer's bytes;
|
|
461
|
+
* the immutable base keeps the original. The package README explains the consequences.
|
|
462
|
+
* `config` is the settings the app registers, over {@link REDACTION_DEFAULTS}.
|
|
223
463
|
*/
|
|
224
|
-
const redactionPlugin = () => (0, _embedpdf_core.definePlugin)({
|
|
464
|
+
const redactionPlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
225
465
|
id: "redaction",
|
|
226
466
|
token: require_contract.RedactionToken,
|
|
227
467
|
scope: "document",
|
|
228
468
|
requires: [_embedpdf_plugin_annotation_contract_host.AnnotationToken],
|
|
229
|
-
optional: [
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
469
|
+
optional: [_embedpdf_plugin_selection_contract.SelectionToken, _embedpdf_plugin_search_contract.SearchToken],
|
|
470
|
+
state: initialRedactionState,
|
|
471
|
+
settings: {
|
|
472
|
+
defaults: require_contract.REDACTION_DEFAULTS,
|
|
473
|
+
registered: config
|
|
474
|
+
},
|
|
475
|
+
create: createRedactionController
|
|
476
|
+
});
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/state.ts
|
|
479
|
+
/**
|
|
480
|
+
* The redaction page's State table as code: what `useRedactionState()`
|
|
481
|
+
* returns, and the same fields in every other framework. The marks
|
|
482
|
+
* themselves are not here: `usePendingRedactions()` reads them, so a
|
|
483
|
+
* component that only shows the count doesn't re-render for every mark.
|
|
484
|
+
*/
|
|
485
|
+
const redactionState = (0, _embedpdf_core.defineState)(require_contract.RedactionToken, {
|
|
486
|
+
read: (redaction) => ({
|
|
487
|
+
pendingCount: redaction.getPendingCount(),
|
|
488
|
+
applying: redaction.isApplying(),
|
|
489
|
+
lastResult: redaction.getLastResult()
|
|
490
|
+
}),
|
|
491
|
+
empty: {
|
|
492
|
+
pendingCount: 0,
|
|
493
|
+
applying: false,
|
|
494
|
+
lastResult: null
|
|
495
|
+
}
|
|
233
496
|
});
|
|
234
497
|
//#endregion
|
|
498
|
+
exports.REDACTION_DEFAULTS = require_contract.REDACTION_DEFAULTS;
|
|
235
499
|
exports.RedactionToken = require_contract.RedactionToken;
|
|
236
500
|
exports.redactionPlugin = redactionPlugin;
|
|
501
|
+
exports.redactionState = redactionState;
|
|
237
502
|
|
|
238
503
|
//# sourceMappingURL=index.cjs.map
|