@nextcloud/image-editor 1.0.0-beta.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/index.cjs ADDED
@@ -0,0 +1,4690 @@
1
+ require('./assets/index.css');
2
+ "use strict";
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
+ const vue = require("vue");
5
+ const Konva = require("konva");
6
+ const NcButton = require("@nextcloud/vue/components/NcButton");
7
+ const NcLoadingIcon = require("@nextcloud/vue/components/NcLoadingIcon");
8
+ const gettext = require("@nextcloud/l10n/gettext");
9
+ const emoji = require("@nextcloud/vue/functions/emoji");
10
+ const NcEmojiPicker = require("@nextcloud/vue/components/NcEmojiPicker");
11
+ const dialogs = require("@nextcloud/dialogs");
12
+ const NcActionButton = require("@nextcloud/vue/components/NcActionButton");
13
+ const NcActions = require("@nextcloud/vue/components/NcActions");
14
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
15
+ const Konva__default = /* @__PURE__ */ _interopDefault(Konva);
16
+ const NcButton__default = /* @__PURE__ */ _interopDefault(NcButton);
17
+ const NcLoadingIcon__default = /* @__PURE__ */ _interopDefault(NcLoadingIcon);
18
+ const NcEmojiPicker__default = /* @__PURE__ */ _interopDefault(NcEmojiPicker);
19
+ const NcActionButton__default = /* @__PURE__ */ _interopDefault(NcActionButton);
20
+ const NcActions__default = /* @__PURE__ */ _interopDefault(NcActions);
21
+ function useHistory(capacity = 100) {
22
+ if (!Number.isInteger(capacity) || capacity < 1) {
23
+ throw new RangeError("History capacity must be a positive integer");
24
+ }
25
+ const entries = vue.shallowRef([]);
26
+ const index = vue.shallowRef(-1);
27
+ const canUndo = vue.computed(() => index.value > 0);
28
+ const canRedo = vue.computed(() => index.value < entries.value.length - 1);
29
+ const current = vue.computed(() => entries.value[index.value]?.snapshot);
30
+ function push(snapshot, label) {
31
+ const kept = [...entries.value.slice(0, index.value + 1), { snapshot, label }];
32
+ entries.value = kept.slice(Math.max(0, kept.length - capacity));
33
+ index.value = entries.value.length - 1;
34
+ }
35
+ function jumpTo(target) {
36
+ if (target === index.value || target < 0 || target >= entries.value.length) {
37
+ return void 0;
38
+ }
39
+ index.value = target;
40
+ return current.value;
41
+ }
42
+ function undo() {
43
+ return canUndo.value ? jumpTo(index.value - 1) : void 0;
44
+ }
45
+ function redo() {
46
+ return canRedo.value ? jumpTo(index.value + 1) : void 0;
47
+ }
48
+ function clear() {
49
+ entries.value = [];
50
+ index.value = -1;
51
+ }
52
+ return {
53
+ canUndo,
54
+ canRedo,
55
+ current,
56
+ entries: vue.computed(() => entries.value),
57
+ index: vue.computed(() => index.value),
58
+ push,
59
+ undo,
60
+ redo,
61
+ jumpTo,
62
+ clear
63
+ };
64
+ }
65
+ function newId() {
66
+ if (typeof crypto.randomUUID === "function") {
67
+ return crypto.randomUUID();
68
+ }
69
+ return [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
70
+ }
71
+ function createInitialState() {
72
+ return {
73
+ rotation: 0,
74
+ fineRotation: 0,
75
+ zoom: 1,
76
+ flipX: false,
77
+ flipY: false,
78
+ crop: null,
79
+ adjustments: { brightness: 0, contrast: 0, saturation: 0 },
80
+ preset: "none",
81
+ annotations: []
82
+ };
83
+ }
84
+ function isPristine(state) {
85
+ const { adjustments } = state;
86
+ return state.rotation === 0 && state.fineRotation === 0 && state.zoom === 1 && !state.flipX && !state.flipY && state.crop === null && adjustments.brightness === 0 && adjustments.contrast === 0 && adjustments.saturation === 0 && state.preset === "none" && state.annotations.length === 0;
87
+ }
88
+ function orientedSize(natural, rotation) {
89
+ return rotation % 180 === 0 ? { width: natural.width, height: natural.height } : { width: natural.height, height: natural.width };
90
+ }
91
+ function clampRect(rect, bounds) {
92
+ const x = Math.min(Math.max(rect.x, 0), bounds.width - 1);
93
+ const y = Math.min(Math.max(rect.y, 0), bounds.height - 1);
94
+ return {
95
+ x,
96
+ y,
97
+ width: Math.max(1, Math.min(rect.width, bounds.width - x)),
98
+ height: Math.max(1, Math.min(rect.height, bounds.height - y))
99
+ };
100
+ }
101
+ function mapPointsCW(points, height) {
102
+ const mapped = [];
103
+ for (let i = 0; i < points.length; i += 2) {
104
+ mapped.push(height - (points[i + 1] ?? 0), points[i] ?? 0);
105
+ }
106
+ return mapped;
107
+ }
108
+ function mapRectCW(rect, height) {
109
+ return {
110
+ x: height - rect.y - rect.height,
111
+ y: rect.x,
112
+ width: rect.height,
113
+ height: rect.width
114
+ };
115
+ }
116
+ function mapAnnotationCW(annotation, height) {
117
+ switch (annotation.type) {
118
+ case "draw":
119
+ return { ...annotation, points: mapPointsCW(annotation.points, height) };
120
+ case "arrow":
121
+ return { ...annotation, points: mapPointsCW(annotation.points, height) };
122
+ case "rectangle":
123
+ case "ellipse":
124
+ return {
125
+ ...annotation,
126
+ rect: { ...annotation.rect, x: height - annotation.rect.y, y: annotation.rect.x },
127
+ rotation: (annotation.rotation + 90) % 360
128
+ };
129
+ case "redact":
130
+ return { ...annotation, rect: mapRectCW(annotation.rect, height) };
131
+ case "text":
132
+ case "sticker":
133
+ return {
134
+ ...annotation,
135
+ x: height - annotation.y,
136
+ y: annotation.x,
137
+ rotation: (annotation.rotation + 90) % 360
138
+ };
139
+ }
140
+ }
141
+ function rotateCW(state, oriented) {
142
+ return {
143
+ ...state,
144
+ rotation: (state.rotation + 90) % 360,
145
+ crop: state.crop && mapRectCW(state.crop, oriented.height),
146
+ annotations: state.annotations.map((annotation) => mapAnnotationCW(annotation, oriented.height))
147
+ };
148
+ }
149
+ function mapAnnotationFlipX(annotation, width) {
150
+ switch (annotation.type) {
151
+ case "draw":
152
+ case "arrow": {
153
+ const points = annotation.points.map((value, i) => i % 2 === 0 ? width - value : value);
154
+ return { ...annotation, points };
155
+ }
156
+ case "rectangle":
157
+ case "ellipse": {
158
+ const radians = annotation.rotation * Math.PI / 180;
159
+ return {
160
+ ...annotation,
161
+ rect: {
162
+ ...annotation.rect,
163
+ x: width - annotation.rect.x - annotation.rect.width * Math.cos(radians),
164
+ y: annotation.rect.y + annotation.rect.width * Math.sin(radians)
165
+ },
166
+ rotation: (360 - annotation.rotation) % 360
167
+ };
168
+ }
169
+ case "redact":
170
+ return {
171
+ ...annotation,
172
+ rect: { ...annotation.rect, x: width - annotation.rect.x - annotation.rect.width }
173
+ };
174
+ case "text":
175
+ case "sticker":
176
+ return {
177
+ ...annotation,
178
+ x: width - annotation.x,
179
+ rotation: (360 - annotation.rotation) % 360
180
+ };
181
+ }
182
+ }
183
+ function flipHorizontal(state, oriented) {
184
+ const sideways = state.rotation % 180 !== 0;
185
+ return {
186
+ ...state,
187
+ flipX: sideways ? state.flipX : !state.flipX,
188
+ flipY: sideways ? !state.flipY : state.flipY,
189
+ fineRotation: -state.fineRotation,
190
+ crop: state.crop && { ...state.crop, x: oriented.width - state.crop.x - state.crop.width },
191
+ annotations: state.annotations.map((annotation) => mapAnnotationFlipX(annotation, oriented.width))
192
+ };
193
+ }
194
+ function mapAnnotationFlipY(annotation, height) {
195
+ switch (annotation.type) {
196
+ case "draw":
197
+ case "arrow": {
198
+ const points = annotation.points.map((value, i) => i % 2 === 1 ? height - value : value);
199
+ return { ...annotation, points };
200
+ }
201
+ case "rectangle":
202
+ case "ellipse": {
203
+ const radians = annotation.rotation * Math.PI / 180;
204
+ return {
205
+ ...annotation,
206
+ rect: {
207
+ ...annotation.rect,
208
+ x: annotation.rect.x - annotation.rect.height * Math.sin(radians),
209
+ y: height - annotation.rect.y - annotation.rect.height * Math.cos(radians)
210
+ },
211
+ rotation: (360 - annotation.rotation) % 360
212
+ };
213
+ }
214
+ case "redact":
215
+ return {
216
+ ...annotation,
217
+ rect: { ...annotation.rect, y: height - annotation.rect.y - annotation.rect.height }
218
+ };
219
+ case "text":
220
+ case "sticker":
221
+ return {
222
+ ...annotation,
223
+ y: height - annotation.y,
224
+ rotation: (360 - annotation.rotation) % 360
225
+ };
226
+ }
227
+ }
228
+ function flipVertical(state, oriented) {
229
+ const sideways = state.rotation % 180 !== 0;
230
+ return {
231
+ ...state,
232
+ flipX: sideways ? !state.flipX : state.flipX,
233
+ flipY: sideways ? state.flipY : !state.flipY,
234
+ fineRotation: -state.fineRotation,
235
+ crop: state.crop && { ...state.crop, y: oriented.height - state.crop.y - state.crop.height },
236
+ annotations: state.annotations.map((annotation) => mapAnnotationFlipY(annotation, oriented.height))
237
+ };
238
+ }
239
+ function translateAnnotation(annotation, dx, dy) {
240
+ switch (annotation.type) {
241
+ case "draw":
242
+ case "arrow": {
243
+ const points = annotation.points.map((value, i) => value + (i % 2 === 0 ? dx : dy));
244
+ return { ...annotation, points };
245
+ }
246
+ case "rectangle":
247
+ case "ellipse":
248
+ case "redact":
249
+ return {
250
+ ...annotation,
251
+ rect: { ...annotation.rect, x: annotation.rect.x + dx, y: annotation.rect.y + dy }
252
+ };
253
+ case "text":
254
+ case "sticker":
255
+ return { ...annotation, x: annotation.x + dx, y: annotation.y + dy };
256
+ }
257
+ }
258
+ function duplicateAnnotation(annotation, offset = 16) {
259
+ return { ...translateAnnotation(annotation, offset, offset), id: newId() };
260
+ }
261
+ const _sfc_main$I = /* @__PURE__ */ vue.defineComponent({
262
+ __name: "GlassSurface",
263
+ props: {
264
+ variant: {}
265
+ },
266
+ setup(__props) {
267
+ return (_ctx, _cache) => {
268
+ return vue.openBlock(), vue.createElementBlock("div", {
269
+ class: vue.normalizeClass(["glass-surface", `glass-surface--${__props.variant ?? "card"}`])
270
+ }, [
271
+ vue.renderSlot(_ctx.$slots, "default", {}, void 0, true)
272
+ ], 2);
273
+ };
274
+ }
275
+ });
276
+ const _export_sfc = (sfc, props) => {
277
+ const target = sfc.__vccOpts || sfc;
278
+ for (const [key, val] of props) {
279
+ target[key] = val;
280
+ }
281
+ return target;
282
+ };
283
+ const GlassSurface = /* @__PURE__ */ _export_sfc(_sfc_main$I, [["__scopeId", "data-v-b60e81d9"]]);
284
+ const _sfc_main$H = {
285
+ name: "ContrastCircleIcon",
286
+ emits: ["click"],
287
+ props: {
288
+ title: {
289
+ type: String
290
+ },
291
+ fillColor: {
292
+ type: String,
293
+ default: "currentColor"
294
+ },
295
+ size: {
296
+ type: Number,
297
+ default: 24
298
+ }
299
+ }
300
+ };
301
+ const _hoisted_1$E = ["aria-hidden", "aria-label"];
302
+ const _hoisted_2$z = ["fill", "width", "height"];
303
+ const _hoisted_3$w = { d: "M12,20C9.79,20 7.79,19.1 6.34,17.66L17.66,6.34C19.1,7.79 20,9.79 20,12A8,8 0 0,1 12,20M6,8H8V6H9.5V8H11.5V9.5H9.5V11.5H8V9.5H6M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,16H17V14.5H12V16Z" };
304
+ const _hoisted_4$v = { key: 0 };
305
+ function _sfc_render$r(_ctx, _cache, $props, $setup, $data, $options) {
306
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
307
+ "aria-hidden": $props.title ? null : "true",
308
+ "aria-label": $props.title,
309
+ class: "material-design-icon contrast-circle-icon",
310
+ role: "img",
311
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
312
+ }), [
313
+ (vue.openBlock(), vue.createElementBlock("svg", {
314
+ fill: $props.fillColor,
315
+ class: "material-design-icon__svg",
316
+ width: $props.size,
317
+ height: $props.size,
318
+ viewBox: "0 0 24 24"
319
+ }, [
320
+ vue.createElementVNode("path", _hoisted_3$w, [
321
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$v, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
322
+ ])
323
+ ], 8, _hoisted_2$z))
324
+ ], 16, _hoisted_1$E);
325
+ }
326
+ const ContrastCircle = /* @__PURE__ */ _export_sfc(_sfc_main$H, [["render", _sfc_render$r]]);
327
+ const _sfc_main$G = {
328
+ name: "InvertColorsIcon",
329
+ emits: ["click"],
330
+ props: {
331
+ title: {
332
+ type: String
333
+ },
334
+ fillColor: {
335
+ type: String,
336
+ default: "currentColor"
337
+ },
338
+ size: {
339
+ type: Number,
340
+ default: 24
341
+ }
342
+ }
343
+ };
344
+ const _hoisted_1$D = ["aria-hidden", "aria-label"];
345
+ const _hoisted_2$y = ["fill", "width", "height"];
346
+ const _hoisted_3$v = { d: "M12,19.58V19.58C10.4,19.58 8.89,18.96 7.76,17.83C6.62,16.69 6,15.19 6,13.58C6,12 6.62,10.47 7.76,9.34L12,5.1M17.66,7.93L12,2.27V2.27L6.34,7.93C3.22,11.05 3.22,16.12 6.34,19.24C7.9,20.8 9.95,21.58 12,21.58C14.05,21.58 16.1,20.8 17.66,19.24C20.78,16.12 20.78,11.05 17.66,7.93Z" };
347
+ const _hoisted_4$u = { key: 0 };
348
+ function _sfc_render$q(_ctx, _cache, $props, $setup, $data, $options) {
349
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
350
+ "aria-hidden": $props.title ? null : "true",
351
+ "aria-label": $props.title,
352
+ class: "material-design-icon invert-colors-icon",
353
+ role: "img",
354
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
355
+ }), [
356
+ (vue.openBlock(), vue.createElementBlock("svg", {
357
+ fill: $props.fillColor,
358
+ class: "material-design-icon__svg",
359
+ width: $props.size,
360
+ height: $props.size,
361
+ viewBox: "0 0 24 24"
362
+ }, [
363
+ vue.createElementVNode("path", _hoisted_3$v, [
364
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$u, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
365
+ ])
366
+ ], 8, _hoisted_2$y))
367
+ ], 16, _hoisted_1$D);
368
+ }
369
+ const InvertColors = /* @__PURE__ */ _export_sfc(_sfc_main$G, [["render", _sfc_render$q]]);
370
+ const _sfc_main$F = {
371
+ name: "WhiteBalanceSunnyIcon",
372
+ emits: ["click"],
373
+ props: {
374
+ title: {
375
+ type: String
376
+ },
377
+ fillColor: {
378
+ type: String,
379
+ default: "currentColor"
380
+ },
381
+ size: {
382
+ type: Number,
383
+ default: 24
384
+ }
385
+ }
386
+ };
387
+ const _hoisted_1$C = ["aria-hidden", "aria-label"];
388
+ const _hoisted_2$x = ["fill", "width", "height"];
389
+ const _hoisted_3$u = { d: "M3.55 19.09L4.96 20.5L6.76 18.71L5.34 17.29M12 6C8.69 6 6 8.69 6 12S8.69 18 12 18 18 15.31 18 12C18 8.68 15.31 6 12 6M20 13H23V11H20M17.24 18.71L19.04 20.5L20.45 19.09L18.66 17.29M20.45 5L19.04 3.6L17.24 5.39L18.66 6.81M13 1H11V4H13M6.76 5.39L4.96 3.6L3.55 5L5.34 6.81L6.76 5.39M1 13H4V11H1M13 20H11V23H13" };
390
+ const _hoisted_4$t = { key: 0 };
391
+ function _sfc_render$p(_ctx, _cache, $props, $setup, $data, $options) {
392
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
393
+ "aria-hidden": $props.title ? null : "true",
394
+ "aria-label": $props.title,
395
+ class: "material-design-icon white-balance-sunny-icon",
396
+ role: "img",
397
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
398
+ }), [
399
+ (vue.openBlock(), vue.createElementBlock("svg", {
400
+ fill: $props.fillColor,
401
+ class: "material-design-icon__svg",
402
+ width: $props.size,
403
+ height: $props.size,
404
+ viewBox: "0 0 24 24"
405
+ }, [
406
+ vue.createElementVNode("path", _hoisted_3$u, [
407
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$t, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
408
+ ])
409
+ ], 8, _hoisted_2$x))
410
+ ], 16, _hoisted_1$C);
411
+ }
412
+ const WhiteBalanceSunny = /* @__PURE__ */ _export_sfc(_sfc_main$F, [["render", _sfc_render$p]]);
413
+ const _hoisted_1$B = { class: "editor-slider" };
414
+ const _hoisted_2$w = ["value", "data-test", "disabled", "aria-label", "min", "max", "step"];
415
+ const _sfc_main$E = /* @__PURE__ */ vue.defineComponent({
416
+ __name: "EditorSlider",
417
+ props: {
418
+ value: {},
419
+ min: {},
420
+ max: {},
421
+ step: {},
422
+ label: {},
423
+ display: {},
424
+ dataTest: {},
425
+ disabled: { type: Boolean }
426
+ },
427
+ emits: ["input", "commit"],
428
+ setup(__props, { emit: __emit }) {
429
+ const props = __props;
430
+ const emit = __emit;
431
+ function onInput(event) {
432
+ emit("input", Number(event.target.value));
433
+ }
434
+ return (_ctx, _cache) => {
435
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$B, [
436
+ vue.createElementVNode("input", {
437
+ value: props.value,
438
+ "data-test": __props.dataTest,
439
+ disabled: __props.disabled,
440
+ "aria-label": __props.label,
441
+ type: "range",
442
+ min: __props.min,
443
+ max: __props.max,
444
+ step: __props.step,
445
+ onInput,
446
+ onChange: _cache[0] || (_cache[0] = ($event) => emit("commit"))
447
+ }, null, 40, _hoisted_2$w),
448
+ vue.createElementVNode("output", null, [
449
+ vue.renderSlot(_ctx.$slots, "preview", {}, () => [
450
+ vue.createTextVNode(vue.toDisplayString(__props.display ?? props.value), 1)
451
+ ], true)
452
+ ])
453
+ ]);
454
+ };
455
+ }
456
+ });
457
+ const EditorSlider = /* @__PURE__ */ _export_sfc(_sfc_main$E, [["__scopeId", "data-v-6562ea60"]]);
458
+ const _hoisted_1$A = ["disabled", "data-test", "aria-pressed"];
459
+ const _sfc_main$D = /* @__PURE__ */ vue.defineComponent({
460
+ __name: "IconTab",
461
+ props: {
462
+ label: {},
463
+ active: { type: Boolean },
464
+ disabled: { type: Boolean },
465
+ dataTest: {}
466
+ },
467
+ emits: ["click"],
468
+ setup(__props, { emit: __emit }) {
469
+ const emit = __emit;
470
+ return (_ctx, _cache) => {
471
+ return vue.openBlock(), vue.createElementBlock("button", {
472
+ type: "button",
473
+ class: vue.normalizeClass(["icon-tab", { "icon-tab--active": __props.active }]),
474
+ disabled: __props.disabled,
475
+ "data-test": __props.dataTest,
476
+ "aria-pressed": __props.active,
477
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
478
+ }, [
479
+ vue.renderSlot(_ctx.$slots, "default", {}, void 0, true),
480
+ vue.createElementVNode("span", null, vue.toDisplayString(__props.label), 1)
481
+ ], 10, _hoisted_1$A);
482
+ };
483
+ }
484
+ });
485
+ const IconTab = /* @__PURE__ */ _export_sfc(_sfc_main$D, [["__scopeId", "data-v-95889662"]]);
486
+ const gtBuilder = gettext.getGettextBuilder().detectLocale();
487
+ [].map((data) => gtBuilder.addTranslation(data.locale, data.json));
488
+ const gt = gtBuilder.build();
489
+ gt.ngettext.bind(gt);
490
+ const t = gt.gettext.bind(gt);
491
+ const VIEW_MARGIN = 16;
492
+ const MIN_ZOOM = 1;
493
+ const MAX_ZOOM = 4;
494
+ const ZOOM_SNAP = 1.05;
495
+ const PINCH_TOLERANCE = 0.01;
496
+ const WHEEL_LINE = 16;
497
+ const WHEEL_CLAMP = 300;
498
+ const WHEEL_SENSITIVITY = 15e-4;
499
+ function panBounds(visible, scale, container) {
500
+ return {
501
+ x: Math.max(0, (visible.width * scale - container.width) / 2 + VIEW_MARGIN),
502
+ y: Math.max(0, (visible.height * scale - container.height) / 2 + VIEW_MARGIN)
503
+ };
504
+ }
505
+ function clampPan(pan, bounds) {
506
+ return {
507
+ x: Math.min(bounds.x, Math.max(-bounds.x, pan.x)),
508
+ y: Math.min(bounds.y, Math.max(-bounds.y, pan.y))
509
+ };
510
+ }
511
+ function clampZoom(zoom) {
512
+ return zoom < ZOOM_SNAP ? MIN_ZOOM : Math.min(MAX_ZOOM, zoom);
513
+ }
514
+ function anchoredPan(pan, cursor, factor) {
515
+ return {
516
+ x: cursor.x - factor * (cursor.x - pan.x),
517
+ y: cursor.y - factor * (cursor.y - pan.y)
518
+ };
519
+ }
520
+ function wheelZoomFactor(deltaY, deltaMode, pageHeight) {
521
+ const scaled = deltaMode === 1 ? deltaY * WHEEL_LINE : deltaMode === 2 ? deltaY * pageHeight : deltaY;
522
+ const bounded = Math.min(WHEEL_CLAMP, Math.max(-WHEEL_CLAMP, scaled));
523
+ return Math.exp(-bounded * WHEEL_SENSITIVITY);
524
+ }
525
+ const MODE_DEFAULT_TOOL = {
526
+ select: "select",
527
+ crop: "crop",
528
+ finetune: "adjust",
529
+ filter: "adjust",
530
+ annotate: "draw",
531
+ sticker: "sticker",
532
+ redact: "redact"
533
+ };
534
+ const EDITOR_CONTEXT = /* @__PURE__ */ Symbol("nextcloud:image-editor");
535
+ function createEditorContext() {
536
+ const history = useHistory();
537
+ const state = vue.shallowRef(createInitialState());
538
+ const viewZoom = vue.shallowRef(MIN_ZOOM);
539
+ const viewPan = vue.shallowRef({ x: 0, y: 0 });
540
+ const viewFit = vue.shallowRef(null);
541
+ const boundsAt = (zoom) => {
542
+ const fit = viewFit.value;
543
+ return fit === null ? { x: 0, y: 0 } : panBounds(fit.visible, fit.scale * zoom, fit.container);
544
+ };
545
+ const activeMode = vue.shallowRef("crop");
546
+ const activeTool = vue.shallowRef(MODE_DEFAULT_TOOL.crop);
547
+ history.push(state.value, t("Original"));
548
+ const context = {
549
+ state,
550
+ activeMode,
551
+ activeTool,
552
+ setMode(mode) {
553
+ activeMode.value = mode;
554
+ activeTool.value = MODE_DEFAULT_TOOL[mode];
555
+ context.selectedId.value = null;
556
+ },
557
+ drawColor: vue.shallowRef("#ff0000"),
558
+ strokeWidth: vue.shallowRef(6),
559
+ fontSize: vue.shallowRef(24),
560
+ sticker: vue.shallowRef("😀"),
561
+ redactStyle: vue.shallowRef("pixelate"),
562
+ cropAspect: vue.shallowRef(null),
563
+ viewZoom,
564
+ viewPan,
565
+ viewFit,
566
+ panning: vue.shallowRef(false),
567
+ setViewZoom(zoom, anchor) {
568
+ const previous = viewZoom.value;
569
+ const next = clampZoom(zoom);
570
+ viewZoom.value = next;
571
+ if (next === MIN_ZOOM) {
572
+ viewPan.value = { x: 0, y: 0 };
573
+ return;
574
+ }
575
+ const panned = anchor === void 0 ? viewPan.value : anchoredPan(viewPan.value, anchor, next / previous);
576
+ viewPan.value = clampPan(panned, boundsAt(next));
577
+ },
578
+ setViewPan(pan) {
579
+ viewPan.value = clampPan(pan, boundsAt(viewZoom.value));
580
+ },
581
+ interacting: vue.shallowRef(false),
582
+ selectedId: vue.shallowRef(null),
583
+ canUndo: history.canUndo,
584
+ canRedo: history.canRedo,
585
+ historyEntries: history.entries,
586
+ historyIndex: history.index,
587
+ commit(next, label) {
588
+ context.interacting.value = false;
589
+ state.value = next;
590
+ history.push(next, label);
591
+ },
592
+ preview(next) {
593
+ context.interacting.value = true;
594
+ state.value = next;
595
+ },
596
+ undo() {
597
+ const snapshot = history.undo();
598
+ if (snapshot !== void 0) {
599
+ state.value = snapshot;
600
+ }
601
+ },
602
+ redo() {
603
+ const snapshot = history.redo();
604
+ if (snapshot !== void 0) {
605
+ state.value = snapshot;
606
+ }
607
+ },
608
+ jumpTo(index) {
609
+ const snapshot = history.jumpTo(index);
610
+ if (snapshot !== void 0) {
611
+ state.value = snapshot;
612
+ }
613
+ },
614
+ reset(next) {
615
+ history.clear();
616
+ state.value = next ?? createInitialState();
617
+ activeMode.value = "crop";
618
+ activeTool.value = MODE_DEFAULT_TOOL.crop;
619
+ context.selectedId.value = null;
620
+ viewZoom.value = MIN_ZOOM;
621
+ viewPan.value = { x: 0, y: 0 };
622
+ context.panning.value = false;
623
+ history.push(state.value, next === void 0 ? t("Original") : t("Restored"));
624
+ }
625
+ };
626
+ vue.watch(activeTool, (tool) => {
627
+ if (tool !== "select") {
628
+ context.selectedId.value = null;
629
+ }
630
+ });
631
+ vue.provide(EDITOR_CONTEXT, context);
632
+ return context;
633
+ }
634
+ function useEditorContext() {
635
+ const context = vue.inject(EDITOR_CONTEXT, null);
636
+ if (context === null) {
637
+ throw new Error("useEditorContext() called outside of an ImageEditor tree");
638
+ }
639
+ return context;
640
+ }
641
+ const _hoisted_1$z = { class: "adjust-panel" };
642
+ const _hoisted_2$v = { class: "adjust-panel__tabs" };
643
+ const _sfc_main$C = /* @__PURE__ */ vue.defineComponent({
644
+ __name: "AdjustPanel",
645
+ props: {
646
+ loaded: { type: Boolean }
647
+ },
648
+ setup(__props) {
649
+ const context = useEditorContext();
650
+ const adjustments = [
651
+ { id: "brightness", label: t("Brightness"), icon: WhiteBalanceSunny },
652
+ { id: "contrast", label: t("Contrast"), icon: ContrastCircle },
653
+ { id: "saturation", label: t("Saturation"), icon: InvertColors }
654
+ ];
655
+ const activeAdjustment = vue.shallowRef("brightness");
656
+ const display = vue.computed(() => {
657
+ const value = context.state.value.adjustments[activeAdjustment.value];
658
+ return value > 0 ? `+${value}` : `${value}`;
659
+ });
660
+ function onAdjustInput(value) {
661
+ const state = context.state.value;
662
+ context.preview({
663
+ ...state,
664
+ adjustments: { ...state.adjustments, [activeAdjustment.value]: value }
665
+ });
666
+ }
667
+ function onSliderCommit() {
668
+ context.commit(context.state.value, adjustments.find((entry) => entry.id === activeAdjustment.value).label);
669
+ }
670
+ return (_ctx, _cache) => {
671
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$z, [
672
+ vue.createElementVNode("div", _hoisted_2$v, [
673
+ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(adjustments, (adjustment) => {
674
+ return vue.createVNode(IconTab, {
675
+ key: adjustment.id,
676
+ label: adjustment.label,
677
+ active: activeAdjustment.value === adjustment.id,
678
+ disabled: !__props.loaded,
679
+ "data-test": `tab-${adjustment.id}`,
680
+ onClick: ($event) => activeAdjustment.value = adjustment.id
681
+ }, {
682
+ default: vue.withCtx(() => [
683
+ (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(adjustment.icon), { size: 20 }))
684
+ ]),
685
+ _: 2
686
+ }, 1032, ["label", "active", "disabled", "data-test", "onClick"]);
687
+ }), 64))
688
+ ]),
689
+ vue.createVNode(EditorSlider, {
690
+ value: vue.unref(context).state.value.adjustments[activeAdjustment.value],
691
+ min: -100,
692
+ max: 100,
693
+ step: 1,
694
+ label: adjustments.find((entry) => entry.id === activeAdjustment.value).label,
695
+ display: display.value,
696
+ "data-test": `adjust-${activeAdjustment.value}`,
697
+ disabled: !__props.loaded,
698
+ onInput: onAdjustInput,
699
+ onCommit: onSliderCommit
700
+ }, null, 8, ["value", "label", "display", "data-test", "disabled"])
701
+ ]);
702
+ };
703
+ }
704
+ });
705
+ const AdjustPanel = /* @__PURE__ */ _export_sfc(_sfc_main$C, [["__scopeId", "data-v-d7f848cf"]]);
706
+ const _sfc_main$B = {
707
+ name: "ArrowTopRightIcon",
708
+ emits: ["click"],
709
+ props: {
710
+ title: {
711
+ type: String
712
+ },
713
+ fillColor: {
714
+ type: String,
715
+ default: "currentColor"
716
+ },
717
+ size: {
718
+ type: Number,
719
+ default: 24
720
+ }
721
+ }
722
+ };
723
+ const _hoisted_1$y = ["aria-hidden", "aria-label"];
724
+ const _hoisted_2$u = ["fill", "width", "height"];
725
+ const _hoisted_3$t = { d: "M5,17.59L15.59,7H9V5H19V15H17V8.41L6.41,19L5,17.59Z" };
726
+ const _hoisted_4$s = { key: 0 };
727
+ function _sfc_render$o(_ctx, _cache, $props, $setup, $data, $options) {
728
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
729
+ "aria-hidden": $props.title ? null : "true",
730
+ "aria-label": $props.title,
731
+ class: "material-design-icon arrow-top-right-icon",
732
+ role: "img",
733
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
734
+ }), [
735
+ (vue.openBlock(), vue.createElementBlock("svg", {
736
+ fill: $props.fillColor,
737
+ class: "material-design-icon__svg",
738
+ width: $props.size,
739
+ height: $props.size,
740
+ viewBox: "0 0 24 24"
741
+ }, [
742
+ vue.createElementVNode("path", _hoisted_3$t, [
743
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$s, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
744
+ ])
745
+ ], 8, _hoisted_2$u))
746
+ ], 16, _hoisted_1$y);
747
+ }
748
+ const ArrowTopRight = /* @__PURE__ */ _export_sfc(_sfc_main$B, [["render", _sfc_render$o]]);
749
+ const _sfc_main$A = {
750
+ name: "EllipseOutlineIcon",
751
+ emits: ["click"],
752
+ props: {
753
+ title: {
754
+ type: String
755
+ },
756
+ fillColor: {
757
+ type: String,
758
+ default: "currentColor"
759
+ },
760
+ size: {
761
+ type: Number,
762
+ default: 24
763
+ }
764
+ }
765
+ };
766
+ const _hoisted_1$x = ["aria-hidden", "aria-label"];
767
+ const _hoisted_2$t = ["fill", "width", "height"];
768
+ const _hoisted_3$s = { d: "M12,6C16.41,6 20,8.69 20,12C20,15.31 16.41,18 12,18C7.59,18 4,15.31 4,12C4,8.69 7.59,6 12,6M12,4C6.5,4 2,7.58 2,12C2,16.42 6.5,20 12,20C17.5,20 22,16.42 22,12C22,7.58 17.5,4 12,4Z" };
769
+ const _hoisted_4$r = { key: 0 };
770
+ function _sfc_render$n(_ctx, _cache, $props, $setup, $data, $options) {
771
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
772
+ "aria-hidden": $props.title ? null : "true",
773
+ "aria-label": $props.title,
774
+ class: "material-design-icon ellipse-outline-icon",
775
+ role: "img",
776
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
777
+ }), [
778
+ (vue.openBlock(), vue.createElementBlock("svg", {
779
+ fill: $props.fillColor,
780
+ class: "material-design-icon__svg",
781
+ width: $props.size,
782
+ height: $props.size,
783
+ viewBox: "0 0 24 24"
784
+ }, [
785
+ vue.createElementVNode("path", _hoisted_3$s, [
786
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$r, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
787
+ ])
788
+ ], 8, _hoisted_2$t))
789
+ ], 16, _hoisted_1$x);
790
+ }
791
+ const EllipseOutline = /* @__PURE__ */ _export_sfc(_sfc_main$A, [["render", _sfc_render$n]]);
792
+ const _sfc_main$z = {
793
+ name: "FormatTextIcon",
794
+ emits: ["click"],
795
+ props: {
796
+ title: {
797
+ type: String
798
+ },
799
+ fillColor: {
800
+ type: String,
801
+ default: "currentColor"
802
+ },
803
+ size: {
804
+ type: Number,
805
+ default: 24
806
+ }
807
+ }
808
+ };
809
+ const _hoisted_1$w = ["aria-hidden", "aria-label"];
810
+ const _hoisted_2$s = ["fill", "width", "height"];
811
+ const _hoisted_3$r = { d: "M18.5,4L19.66,8.35L18.7,8.61C18.25,7.74 17.79,6.87 17.26,6.43C16.73,6 16.11,6 15.5,6H13V16.5C13,17 13,17.5 13.33,17.75C13.67,18 14.33,18 15,18V19H9V18C9.67,18 10.33,18 10.67,17.75C11,17.5 11,17 11,16.5V6H8.5C7.89,6 7.27,6 6.74,6.43C6.21,6.87 5.75,7.74 5.3,8.61L4.34,8.35L5.5,4H18.5Z" };
812
+ const _hoisted_4$q = { key: 0 };
813
+ function _sfc_render$m(_ctx, _cache, $props, $setup, $data, $options) {
814
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
815
+ "aria-hidden": $props.title ? null : "true",
816
+ "aria-label": $props.title,
817
+ class: "material-design-icon format-text-icon",
818
+ role: "img",
819
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
820
+ }), [
821
+ (vue.openBlock(), vue.createElementBlock("svg", {
822
+ fill: $props.fillColor,
823
+ class: "material-design-icon__svg",
824
+ width: $props.size,
825
+ height: $props.size,
826
+ viewBox: "0 0 24 24"
827
+ }, [
828
+ vue.createElementVNode("path", _hoisted_3$r, [
829
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$q, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
830
+ ])
831
+ ], 8, _hoisted_2$s))
832
+ ], 16, _hoisted_1$w);
833
+ }
834
+ const FormatText = /* @__PURE__ */ _export_sfc(_sfc_main$z, [["render", _sfc_render$m]]);
835
+ const _sfc_main$y = {
836
+ name: "PencilIcon",
837
+ emits: ["click"],
838
+ props: {
839
+ title: {
840
+ type: String
841
+ },
842
+ fillColor: {
843
+ type: String,
844
+ default: "currentColor"
845
+ },
846
+ size: {
847
+ type: Number,
848
+ default: 24
849
+ }
850
+ }
851
+ };
852
+ const _hoisted_1$v = ["aria-hidden", "aria-label"];
853
+ const _hoisted_2$r = ["fill", "width", "height"];
854
+ const _hoisted_3$q = { d: "M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" };
855
+ const _hoisted_4$p = { key: 0 };
856
+ function _sfc_render$l(_ctx, _cache, $props, $setup, $data, $options) {
857
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
858
+ "aria-hidden": $props.title ? null : "true",
859
+ "aria-label": $props.title,
860
+ class: "material-design-icon pencil-icon",
861
+ role: "img",
862
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
863
+ }), [
864
+ (vue.openBlock(), vue.createElementBlock("svg", {
865
+ fill: $props.fillColor,
866
+ class: "material-design-icon__svg",
867
+ width: $props.size,
868
+ height: $props.size,
869
+ viewBox: "0 0 24 24"
870
+ }, [
871
+ vue.createElementVNode("path", _hoisted_3$q, [
872
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$p, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
873
+ ])
874
+ ], 8, _hoisted_2$r))
875
+ ], 16, _hoisted_1$v);
876
+ }
877
+ const Pencil = /* @__PURE__ */ _export_sfc(_sfc_main$y, [["render", _sfc_render$l]]);
878
+ const _sfc_main$x = {
879
+ name: "RectangleOutlineIcon",
880
+ emits: ["click"],
881
+ props: {
882
+ title: {
883
+ type: String
884
+ },
885
+ fillColor: {
886
+ type: String,
887
+ default: "currentColor"
888
+ },
889
+ size: {
890
+ type: Number,
891
+ default: 24
892
+ }
893
+ }
894
+ };
895
+ const _hoisted_1$u = ["aria-hidden", "aria-label"];
896
+ const _hoisted_2$q = ["fill", "width", "height"];
897
+ const _hoisted_3$p = { d: "M4,6V19H20V6H4M18,17H6V8H18V17Z" };
898
+ const _hoisted_4$o = { key: 0 };
899
+ function _sfc_render$k(_ctx, _cache, $props, $setup, $data, $options) {
900
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
901
+ "aria-hidden": $props.title ? null : "true",
902
+ "aria-label": $props.title,
903
+ class: "material-design-icon rectangle-outline-icon",
904
+ role: "img",
905
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
906
+ }), [
907
+ (vue.openBlock(), vue.createElementBlock("svg", {
908
+ fill: $props.fillColor,
909
+ class: "material-design-icon__svg",
910
+ width: $props.size,
911
+ height: $props.size,
912
+ viewBox: "0 0 24 24"
913
+ }, [
914
+ vue.createElementVNode("path", _hoisted_3$p, [
915
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$o, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
916
+ ])
917
+ ], 8, _hoisted_2$q))
918
+ ], 16, _hoisted_1$u);
919
+ }
920
+ const RectangleOutline = /* @__PURE__ */ _export_sfc(_sfc_main$x, [["render", _sfc_render$k]]);
921
+ function useAnnotationColor(context) {
922
+ function recolorable() {
923
+ const annotation = context.state.value.annotations.find((entry) => entry.id === context.selectedId.value);
924
+ return annotation !== void 0 && "color" in annotation && annotation.type !== "sticker" ? annotation : void 0;
925
+ }
926
+ function apply(color) {
927
+ const annotation = recolorable();
928
+ if (annotation === void 0 || annotation.color === color) {
929
+ return;
930
+ }
931
+ const state = context.state.value;
932
+ context.preview({
933
+ ...state,
934
+ annotations: state.annotations.map((entry) => entry.id === annotation.id ? { ...entry, color } : entry)
935
+ });
936
+ }
937
+ return {
938
+ preview(color) {
939
+ context.drawColor.value = color;
940
+ apply(color);
941
+ },
942
+ commit(color) {
943
+ context.drawColor.value = color;
944
+ apply(color);
945
+ if (recolorable() !== void 0) {
946
+ context.commit(context.state.value, t("Color"));
947
+ }
948
+ }
949
+ };
950
+ }
951
+ const _hoisted_1$t = { class: "annotate-panel" };
952
+ const _hoisted_2$p = { class: "annotate-panel__row" };
953
+ const _hoisted_3$o = { class: "annotate-panel__option" };
954
+ const _hoisted_4$n = ["value"];
955
+ const FONT_SAMPLE = "A";
956
+ const PREVIEW_CAP = 44;
957
+ const _sfc_main$w = /* @__PURE__ */ vue.defineComponent({
958
+ __name: "AnnotatePanel",
959
+ props: {
960
+ loaded: { type: Boolean }
961
+ },
962
+ setup(__props) {
963
+ const context = useEditorContext();
964
+ const color = useAnnotationColor(context);
965
+ const labels = {
966
+ color: t("Color"),
967
+ strokeWidth: t("Stroke width"),
968
+ fontSize: t("Font size")
969
+ };
970
+ const subTools = [
971
+ { id: "draw", label: t("Draw"), icon: Pencil },
972
+ { id: "rectangle", label: t("Rectangle"), icon: RectangleOutline },
973
+ { id: "ellipse", label: t("Ellipse"), icon: EllipseOutline },
974
+ { id: "arrow", label: t("Arrow"), icon: ArrowTopRight },
975
+ { id: "text", label: t("Text"), icon: FormatText }
976
+ ];
977
+ const showStrokeOptions = vue.computed(() => ["draw", "rectangle", "ellipse", "arrow"].includes(context.activeTool.value));
978
+ const viewScale = vue.computed(() => (context.viewFit.value?.scale ?? 1) * context.viewZoom.value);
979
+ const strokePreview = vue.computed(() => Math.min(PREVIEW_CAP, Math.max(2, context.strokeWidth.value * viewScale.value)));
980
+ const fontPreview = vue.computed(() => Math.min(PREVIEW_CAP, Math.max(8, context.fontSize.value * viewScale.value)));
981
+ return (_ctx, _cache) => {
982
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$t, [
983
+ vue.createElementVNode("div", _hoisted_2$p, [
984
+ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(subTools, (tool) => {
985
+ return vue.createVNode(vue.unref(NcButton__default.default), {
986
+ key: tool.id,
987
+ "aria-label": tool.label,
988
+ title: tool.label,
989
+ disabled: !__props.loaded,
990
+ pressed: vue.unref(context).activeTool.value === tool.id,
991
+ variant: "tertiary",
992
+ onClick: ($event) => vue.unref(context).activeTool.value = tool.id
993
+ }, {
994
+ icon: vue.withCtx(() => [
995
+ (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(tool.icon), { size: 20 }))
996
+ ]),
997
+ _: 2
998
+ }, 1032, ["aria-label", "title", "disabled", "pressed", "onClick"]);
999
+ }), 64)),
1000
+ _cache[4] || (_cache[4] = vue.createElementVNode("span", { class: "annotate-panel__divider" }, null, -1)),
1001
+ vue.createElementVNode("label", _hoisted_3$o, [
1002
+ vue.createTextVNode(vue.toDisplayString(labels.color) + " ", 1),
1003
+ vue.createElementVNode("input", {
1004
+ value: vue.unref(context).drawColor.value,
1005
+ type: "color",
1006
+ "data-test": "color",
1007
+ onInput: _cache[0] || (_cache[0] = ($event) => vue.unref(color).preview($event.target.value)),
1008
+ onChange: _cache[1] || (_cache[1] = ($event) => vue.unref(color).commit($event.target.value))
1009
+ }, null, 40, _hoisted_4$n)
1010
+ ])
1011
+ ]),
1012
+ showStrokeOptions.value ? (vue.openBlock(), vue.createBlock(EditorSlider, {
1013
+ key: 0,
1014
+ value: vue.unref(context).strokeWidth.value,
1015
+ min: 1,
1016
+ max: 32,
1017
+ step: 1,
1018
+ label: labels.strokeWidth,
1019
+ onInput: _cache[2] || (_cache[2] = ($event) => vue.unref(context).strokeWidth.value = $event),
1020
+ onCommit: () => {
1021
+ }
1022
+ }, {
1023
+ preview: vue.withCtx(() => [
1024
+ vue.createElementVNode("span", {
1025
+ class: "annotate-panel__dot",
1026
+ "data-test": "stroke-preview",
1027
+ style: vue.normalizeStyle({
1028
+ inlineSize: `${strokePreview.value}px`,
1029
+ blockSize: `${strokePreview.value}px`,
1030
+ backgroundColor: vue.unref(context).drawColor.value
1031
+ })
1032
+ }, null, 4)
1033
+ ]),
1034
+ _: 1
1035
+ }, 8, ["value", "label"])) : vue.unref(context).activeTool.value === "text" ? (vue.openBlock(), vue.createBlock(EditorSlider, {
1036
+ key: 1,
1037
+ value: vue.unref(context).fontSize.value,
1038
+ min: 8,
1039
+ max: 128,
1040
+ step: 1,
1041
+ label: labels.fontSize,
1042
+ onInput: _cache[3] || (_cache[3] = ($event) => vue.unref(context).fontSize.value = $event),
1043
+ onCommit: () => {
1044
+ }
1045
+ }, {
1046
+ preview: vue.withCtx(() => [
1047
+ vue.createElementVNode("span", {
1048
+ class: "annotate-panel__glyph",
1049
+ "data-test": "font-preview",
1050
+ style: vue.normalizeStyle({
1051
+ fontSize: `${fontPreview.value}px`,
1052
+ color: vue.unref(context).drawColor.value
1053
+ })
1054
+ }, vue.toDisplayString(FONT_SAMPLE), 4)
1055
+ ]),
1056
+ _: 1
1057
+ }, 8, ["value", "label"])) : vue.createCommentVNode("", true)
1058
+ ]);
1059
+ };
1060
+ }
1061
+ });
1062
+ const AnnotatePanel = /* @__PURE__ */ _export_sfc(_sfc_main$w, [["__scopeId", "data-v-1bf135e4"]]);
1063
+ const _sfc_main$v = {
1064
+ name: "FlipHorizontalIcon",
1065
+ emits: ["click"],
1066
+ props: {
1067
+ title: {
1068
+ type: String
1069
+ },
1070
+ fillColor: {
1071
+ type: String,
1072
+ default: "currentColor"
1073
+ },
1074
+ size: {
1075
+ type: Number,
1076
+ default: 24
1077
+ }
1078
+ }
1079
+ };
1080
+ const _hoisted_1$s = ["aria-hidden", "aria-label"];
1081
+ const _hoisted_2$o = ["fill", "width", "height"];
1082
+ const _hoisted_3$n = { d: "M15 21H17V19H15M19 9H21V7H19M3 5V19C3 20.1 3.9 21 5 21H9V19H5V5H9V3H5C3.9 3 3 3.9 3 5M19 3V5H21C21 3.9 20.1 3 19 3M11 23H13V1H11M19 17H21V15H19M15 5H17V3H15M19 13H21V11H19M19 21C20.1 21 21 20.1 21 19H19Z" };
1083
+ const _hoisted_4$m = { key: 0 };
1084
+ function _sfc_render$j(_ctx, _cache, $props, $setup, $data, $options) {
1085
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
1086
+ "aria-hidden": $props.title ? null : "true",
1087
+ "aria-label": $props.title,
1088
+ class: "material-design-icon flip-horizontal-icon",
1089
+ role: "img",
1090
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1091
+ }), [
1092
+ (vue.openBlock(), vue.createElementBlock("svg", {
1093
+ fill: $props.fillColor,
1094
+ class: "material-design-icon__svg",
1095
+ width: $props.size,
1096
+ height: $props.size,
1097
+ viewBox: "0 0 24 24"
1098
+ }, [
1099
+ vue.createElementVNode("path", _hoisted_3$n, [
1100
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$m, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
1101
+ ])
1102
+ ], 8, _hoisted_2$o))
1103
+ ], 16, _hoisted_1$s);
1104
+ }
1105
+ const FlipHorizontal = /* @__PURE__ */ _export_sfc(_sfc_main$v, [["render", _sfc_render$j]]);
1106
+ const _sfc_main$u = {
1107
+ name: "FlipVerticalIcon",
1108
+ emits: ["click"],
1109
+ props: {
1110
+ title: {
1111
+ type: String
1112
+ },
1113
+ fillColor: {
1114
+ type: String,
1115
+ default: "currentColor"
1116
+ },
1117
+ size: {
1118
+ type: Number,
1119
+ default: 24
1120
+ }
1121
+ }
1122
+ };
1123
+ const _hoisted_1$r = ["aria-hidden", "aria-label"];
1124
+ const _hoisted_2$n = ["fill", "width", "height"];
1125
+ const _hoisted_3$m = { d: "M3 15V17H5V15M15 19V21H17V19M19 3H5C3.9 3 3 3.9 3 5V9H5V5H19V9H21V5C21 3.9 20.1 3 19 3M21 19H19V21C20.1 21 21 20.1 21 19M1 11V13H23V11M7 19V21H9V19M19 15V17H21V15M11 19V21H13V19M3 19C3 20.1 3.9 21 5 21V19Z" };
1126
+ const _hoisted_4$l = { key: 0 };
1127
+ function _sfc_render$i(_ctx, _cache, $props, $setup, $data, $options) {
1128
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
1129
+ "aria-hidden": $props.title ? null : "true",
1130
+ "aria-label": $props.title,
1131
+ class: "material-design-icon flip-vertical-icon",
1132
+ role: "img",
1133
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1134
+ }), [
1135
+ (vue.openBlock(), vue.createElementBlock("svg", {
1136
+ fill: $props.fillColor,
1137
+ class: "material-design-icon__svg",
1138
+ width: $props.size,
1139
+ height: $props.size,
1140
+ viewBox: "0 0 24 24"
1141
+ }, [
1142
+ vue.createElementVNode("path", _hoisted_3$m, [
1143
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$l, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
1144
+ ])
1145
+ ], 8, _hoisted_2$n))
1146
+ ], 16, _hoisted_1$r);
1147
+ }
1148
+ const FlipVertical = /* @__PURE__ */ _export_sfc(_sfc_main$u, [["render", _sfc_render$i]]);
1149
+ const _sfc_main$t = {
1150
+ name: "RotateLeftIcon",
1151
+ emits: ["click"],
1152
+ props: {
1153
+ title: {
1154
+ type: String
1155
+ },
1156
+ fillColor: {
1157
+ type: String,
1158
+ default: "currentColor"
1159
+ },
1160
+ size: {
1161
+ type: Number,
1162
+ default: 24
1163
+ }
1164
+ }
1165
+ };
1166
+ const _hoisted_1$q = ["aria-hidden", "aria-label"];
1167
+ const _hoisted_2$m = ["fill", "width", "height"];
1168
+ const _hoisted_3$l = { d: "M13,4.07V1L8.45,5.55L13,10V6.09C15.84,6.57 18,9.03 18,12C18,14.97 15.84,17.43 13,17.91V19.93C16.95,19.44 20,16.08 20,12C20,7.92 16.95,4.56 13,4.07M7.1,18.32C8.26,19.22 9.61,19.76 11,19.93V17.9C10.13,17.75 9.29,17.41 8.54,16.87L7.1,18.32M6.09,13H4.07C4.24,14.39 4.79,15.73 5.69,16.89L7.1,15.47C6.58,14.72 6.23,13.88 6.09,13M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11H6.09C6.23,10.13 6.58,9.28 7.11,8.53Z" };
1169
+ const _hoisted_4$k = { key: 0 };
1170
+ function _sfc_render$h(_ctx, _cache, $props, $setup, $data, $options) {
1171
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
1172
+ "aria-hidden": $props.title ? null : "true",
1173
+ "aria-label": $props.title,
1174
+ class: "material-design-icon rotate-left-icon",
1175
+ role: "img",
1176
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1177
+ }), [
1178
+ (vue.openBlock(), vue.createElementBlock("svg", {
1179
+ fill: $props.fillColor,
1180
+ class: "material-design-icon__svg",
1181
+ width: $props.size,
1182
+ height: $props.size,
1183
+ viewBox: "0 0 24 24"
1184
+ }, [
1185
+ vue.createElementVNode("path", _hoisted_3$l, [
1186
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$k, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
1187
+ ])
1188
+ ], 8, _hoisted_2$m))
1189
+ ], 16, _hoisted_1$q);
1190
+ }
1191
+ const RotateLeft = /* @__PURE__ */ _export_sfc(_sfc_main$t, [["render", _sfc_render$h]]);
1192
+ const _sfc_main$s = {
1193
+ name: "RotateRightIcon",
1194
+ emits: ["click"],
1195
+ props: {
1196
+ title: {
1197
+ type: String
1198
+ },
1199
+ fillColor: {
1200
+ type: String,
1201
+ default: "currentColor"
1202
+ },
1203
+ size: {
1204
+ type: Number,
1205
+ default: 24
1206
+ }
1207
+ }
1208
+ };
1209
+ const _hoisted_1$p = ["aria-hidden", "aria-label"];
1210
+ const _hoisted_2$l = ["fill", "width", "height"];
1211
+ const _hoisted_3$k = { d: "M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V4.07C7.06,4.56 4,7.92 4,12C4,16.08 7.05,19.44 11,19.93V17.91C8.16,17.43 6,14.97 6,12C6,9.03 8.16,6.57 11,6.09V10L15.55,5.55Z" };
1212
+ const _hoisted_4$j = { key: 0 };
1213
+ function _sfc_render$g(_ctx, _cache, $props, $setup, $data, $options) {
1214
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
1215
+ "aria-hidden": $props.title ? null : "true",
1216
+ "aria-label": $props.title,
1217
+ class: "material-design-icon rotate-right-icon",
1218
+ role: "img",
1219
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1220
+ }), [
1221
+ (vue.openBlock(), vue.createElementBlock("svg", {
1222
+ fill: $props.fillColor,
1223
+ class: "material-design-icon__svg",
1224
+ width: $props.size,
1225
+ height: $props.size,
1226
+ viewBox: "0 0 24 24"
1227
+ }, [
1228
+ vue.createElementVNode("path", _hoisted_3$k, [
1229
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$j, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
1230
+ ])
1231
+ ], 8, _hoisted_2$l))
1232
+ ], 16, _hoisted_1$p);
1233
+ }
1234
+ const RotateRight = /* @__PURE__ */ _export_sfc(_sfc_main$s, [["render", _sfc_render$g]]);
1235
+ const EDITOR_COMMANDS = /* @__PURE__ */ Symbol("nextcloud:image-editor:commands");
1236
+ function provideEditorCommands(commands) {
1237
+ vue.provide(EDITOR_COMMANDS, commands);
1238
+ return commands;
1239
+ }
1240
+ function useEditorCommands() {
1241
+ const commands = vue.inject(EDITOR_COMMANDS, null);
1242
+ if (commands === null) {
1243
+ throw new Error("useEditorCommands() called outside of an ImageEditor tree");
1244
+ }
1245
+ return commands;
1246
+ }
1247
+ const _hoisted_1$o = { class: "crop-panel" };
1248
+ const _hoisted_2$k = { class: "crop-panel__row" };
1249
+ const _hoisted_3$j = { class: "crop-panel__row" };
1250
+ const _sfc_main$r = /* @__PURE__ */ vue.defineComponent({
1251
+ __name: "CropPanel",
1252
+ props: {
1253
+ loaded: { type: Boolean }
1254
+ },
1255
+ setup(__props) {
1256
+ const context = useEditorContext();
1257
+ const commands = useEditorCommands();
1258
+ const labels = {
1259
+ rotateLeft: t("Rotate left"),
1260
+ rotateRight: t("Rotate right"),
1261
+ flipHorizontal: t("Flip horizontal"),
1262
+ flipVertical: t("Flip vertical"),
1263
+ applyCrop: t("Apply crop"),
1264
+ resetCrop: t("Reset crop"),
1265
+ rotation: t("Rotation"),
1266
+ scale: t("Scale")
1267
+ };
1268
+ const aspectPresets = [
1269
+ { id: null, label: t("Free") },
1270
+ { id: "original", label: t("Original") },
1271
+ { id: 1, label: "1:1" },
1272
+ { id: 4 / 3, label: "4:3" },
1273
+ { id: 16 / 9, label: "16:9" }
1274
+ ];
1275
+ const cropControls = [
1276
+ { id: "rotation", label: labels.rotation },
1277
+ { id: "scale", label: labels.scale }
1278
+ ];
1279
+ const activeCropControl = vue.shallowRef("rotation");
1280
+ const display = vue.computed(() => activeCropControl.value === "rotation" ? `${context.state.value.fineRotation}°` : `×${context.state.value.zoom.toFixed(2)}`);
1281
+ function onTransformInput(value) {
1282
+ context.preview(activeCropControl.value === "rotation" ? { ...context.state.value, fineRotation: value } : { ...context.state.value, zoom: value });
1283
+ }
1284
+ function onSliderCommit() {
1285
+ context.commit(context.state.value, activeCropControl.value === "rotation" ? labels.rotation : labels.scale);
1286
+ }
1287
+ return (_ctx, _cache) => {
1288
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$o, [
1289
+ vue.createElementVNode("div", _hoisted_2$k, [
1290
+ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(aspectPresets, (preset) => {
1291
+ return vue.createVNode(vue.unref(NcButton__default.default), {
1292
+ key: String(preset.id),
1293
+ "data-test": `aspect-${preset.id === null ? "free" : preset.id === "original" ? "original" : preset.label}`,
1294
+ pressed: vue.unref(context).cropAspect.value === preset.id,
1295
+ disabled: !__props.loaded,
1296
+ variant: "tertiary",
1297
+ onClick: ($event) => vue.unref(context).cropAspect.value = preset.id
1298
+ }, {
1299
+ default: vue.withCtx(() => [
1300
+ vue.createTextVNode(vue.toDisplayString(preset.label), 1)
1301
+ ]),
1302
+ _: 2
1303
+ }, 1032, ["data-test", "pressed", "disabled", "onClick"]);
1304
+ }), 64))
1305
+ ]),
1306
+ vue.createElementVNode("div", _hoisted_3$j, [
1307
+ vue.createVNode(vue.unref(NcButton__default.default), {
1308
+ "aria-label": labels.rotateLeft,
1309
+ title: labels.rotateLeft,
1310
+ disabled: !__props.loaded,
1311
+ variant: "tertiary",
1312
+ onClick: _cache[0] || (_cache[0] = ($event) => vue.unref(commands).rotateCCW())
1313
+ }, {
1314
+ icon: vue.withCtx(() => [
1315
+ vue.createVNode(RotateLeft, { size: 20 })
1316
+ ]),
1317
+ _: 1
1318
+ }, 8, ["aria-label", "title", "disabled"]),
1319
+ vue.createVNode(vue.unref(NcButton__default.default), {
1320
+ "aria-label": labels.rotateRight,
1321
+ title: labels.rotateRight,
1322
+ disabled: !__props.loaded,
1323
+ variant: "tertiary",
1324
+ onClick: _cache[1] || (_cache[1] = ($event) => vue.unref(commands).rotateCW())
1325
+ }, {
1326
+ icon: vue.withCtx(() => [
1327
+ vue.createVNode(RotateRight, { size: 20 })
1328
+ ]),
1329
+ _: 1
1330
+ }, 8, ["aria-label", "title", "disabled"]),
1331
+ vue.createVNode(vue.unref(NcButton__default.default), {
1332
+ "aria-label": labels.flipHorizontal,
1333
+ title: labels.flipHorizontal,
1334
+ disabled: !__props.loaded,
1335
+ variant: "tertiary",
1336
+ onClick: _cache[2] || (_cache[2] = ($event) => vue.unref(commands).flipHorizontal())
1337
+ }, {
1338
+ icon: vue.withCtx(() => [
1339
+ vue.createVNode(FlipHorizontal, { size: 20 })
1340
+ ]),
1341
+ _: 1
1342
+ }, 8, ["aria-label", "title", "disabled"]),
1343
+ vue.createVNode(vue.unref(NcButton__default.default), {
1344
+ "aria-label": labels.flipVertical,
1345
+ title: labels.flipVertical,
1346
+ disabled: !__props.loaded,
1347
+ variant: "tertiary",
1348
+ onClick: _cache[3] || (_cache[3] = ($event) => vue.unref(commands).flipVertical())
1349
+ }, {
1350
+ icon: vue.withCtx(() => [
1351
+ vue.createVNode(FlipVertical, { size: 20 })
1352
+ ]),
1353
+ _: 1
1354
+ }, 8, ["aria-label", "title", "disabled"]),
1355
+ _cache[6] || (_cache[6] = vue.createElementVNode("span", { class: "crop-panel__divider" }, null, -1)),
1356
+ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(cropControls, (control) => {
1357
+ return vue.createVNode(vue.unref(NcButton__default.default), {
1358
+ key: control.id,
1359
+ "data-test": `tab-${control.id}`,
1360
+ pressed: activeCropControl.value === control.id,
1361
+ disabled: !__props.loaded,
1362
+ variant: "tertiary",
1363
+ onClick: ($event) => activeCropControl.value = control.id
1364
+ }, {
1365
+ default: vue.withCtx(() => [
1366
+ vue.createTextVNode(vue.toDisplayString(control.label), 1)
1367
+ ]),
1368
+ _: 2
1369
+ }, 1032, ["data-test", "pressed", "disabled", "onClick"]);
1370
+ }), 64)),
1371
+ _cache[7] || (_cache[7] = vue.createElementVNode("span", { class: "crop-panel__divider" }, null, -1)),
1372
+ vue.createVNode(vue.unref(NcButton__default.default), {
1373
+ "data-test": "reset-crop",
1374
+ variant: "tertiary",
1375
+ disabled: !__props.loaded || vue.unref(context).state.value.crop === null,
1376
+ onClick: _cache[4] || (_cache[4] = ($event) => vue.unref(commands).resetCrop())
1377
+ }, {
1378
+ default: vue.withCtx(() => [
1379
+ vue.createTextVNode(vue.toDisplayString(labels.resetCrop), 1)
1380
+ ]),
1381
+ _: 1
1382
+ }, 8, ["disabled"]),
1383
+ vue.createVNode(vue.unref(NcButton__default.default), {
1384
+ "data-test": "apply-crop",
1385
+ variant: "secondary",
1386
+ disabled: !__props.loaded,
1387
+ onClick: _cache[5] || (_cache[5] = ($event) => vue.unref(commands).applyCrop())
1388
+ }, {
1389
+ default: vue.withCtx(() => [
1390
+ vue.createTextVNode(vue.toDisplayString(labels.applyCrop), 1)
1391
+ ]),
1392
+ _: 1
1393
+ }, 8, ["disabled"])
1394
+ ]),
1395
+ activeCropControl.value === "rotation" ? (vue.openBlock(), vue.createBlock(EditorSlider, {
1396
+ key: 0,
1397
+ value: vue.unref(context).state.value.fineRotation,
1398
+ min: -45,
1399
+ max: 45,
1400
+ step: 1,
1401
+ label: labels.rotation,
1402
+ display: display.value,
1403
+ "data-test": "fine-rotation",
1404
+ disabled: !__props.loaded,
1405
+ onInput: onTransformInput,
1406
+ onCommit: onSliderCommit
1407
+ }, null, 8, ["value", "label", "display", "disabled"])) : (vue.openBlock(), vue.createBlock(EditorSlider, {
1408
+ key: 1,
1409
+ value: vue.unref(context).state.value.zoom,
1410
+ min: 1,
1411
+ max: 3,
1412
+ step: 0.05,
1413
+ label: labels.scale,
1414
+ display: display.value,
1415
+ "data-test": "zoom",
1416
+ disabled: !__props.loaded,
1417
+ onInput: onTransformInput,
1418
+ onCommit: onSliderCommit
1419
+ }, null, 8, ["value", "label", "display", "disabled"]))
1420
+ ]);
1421
+ };
1422
+ }
1423
+ });
1424
+ const CropPanel = /* @__PURE__ */ _export_sfc(_sfc_main$r, [["__scopeId", "data-v-30fdb9f8"]]);
1425
+ const _hoisted_1$n = ["disabled", "data-test", "aria-pressed", "title"];
1426
+ const _hoisted_2$j = ["src", "alt"];
1427
+ const _sfc_main$q = /* @__PURE__ */ vue.defineComponent({
1428
+ __name: "PresetChip",
1429
+ props: {
1430
+ url: {},
1431
+ label: {},
1432
+ active: { type: Boolean },
1433
+ disabled: { type: Boolean },
1434
+ dataTest: {}
1435
+ },
1436
+ emits: ["click"],
1437
+ setup(__props, { emit: __emit }) {
1438
+ const emit = __emit;
1439
+ return (_ctx, _cache) => {
1440
+ return vue.openBlock(), vue.createElementBlock("button", {
1441
+ type: "button",
1442
+ class: vue.normalizeClass(["preset-chip", { "preset-chip--active": __props.active }]),
1443
+ disabled: __props.disabled,
1444
+ "data-test": __props.dataTest,
1445
+ "aria-pressed": __props.active,
1446
+ title: __props.label,
1447
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
1448
+ }, [
1449
+ vue.createElementVNode("img", {
1450
+ src: __props.url,
1451
+ alt: __props.label
1452
+ }, null, 8, _hoisted_2$j),
1453
+ vue.createElementVNode("span", null, vue.toDisplayString(__props.label), 1)
1454
+ ], 10, _hoisted_1$n);
1455
+ };
1456
+ }
1457
+ });
1458
+ const PresetChip = /* @__PURE__ */ _export_sfc(_sfc_main$q, [["__scopeId", "data-v-b38cdab1"]]);
1459
+ function luma(r, g, b) {
1460
+ return 0.299 * r + 0.587 * g + 0.114 * b;
1461
+ }
1462
+ function saturate(imageData) {
1463
+ const { data } = imageData;
1464
+ const factor = this.saturation() + 1;
1465
+ for (let i = 0; i < data.length; i += 4) {
1466
+ const gray = luma(data[i], data[i + 1], data[i + 2]);
1467
+ data[i] = gray + (data[i] - gray) * factor;
1468
+ data[i + 1] = gray + (data[i + 1] - gray) * factor;
1469
+ data[i + 2] = gray + (data[i + 2] - gray) * factor;
1470
+ }
1471
+ }
1472
+ function warm(imageData) {
1473
+ const { data } = imageData;
1474
+ for (let i = 0; i < data.length; i += 4) {
1475
+ data[i] = Math.min(255, data[i] * 1.12);
1476
+ data[i + 2] = data[i + 2] * 0.9;
1477
+ }
1478
+ }
1479
+ function cool(imageData) {
1480
+ const { data } = imageData;
1481
+ for (let i = 0; i < data.length; i += 4) {
1482
+ data[i] = data[i] * 0.9;
1483
+ data[i + 2] = Math.min(255, data[i + 2] * 1.12);
1484
+ }
1485
+ }
1486
+ function fade(imageData) {
1487
+ const { data } = imageData;
1488
+ for (let i = 0; i < data.length; i += 4) {
1489
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1490
+ data[i] = 28 + (0.7 * data[i] + 0.3 * luma2) * 0.86 * 1.04;
1491
+ data[i + 1] = 28 + (0.7 * data[i + 1] + 0.3 * luma2) * 0.86;
1492
+ data[i + 2] = 28 + (0.7 * data[i + 2] + 0.3 * luma2) * 0.86 * 0.96;
1493
+ }
1494
+ }
1495
+ function noir(imageData) {
1496
+ const { data } = imageData;
1497
+ for (let i = 0; i < data.length; i += 4) {
1498
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1499
+ const value = Math.min(255, Math.max(0, (luma2 - 128) * 1.35 + 118));
1500
+ data[i] = value;
1501
+ data[i + 1] = value;
1502
+ data[i + 2] = value;
1503
+ }
1504
+ }
1505
+ function golden(imageData) {
1506
+ const { data } = imageData;
1507
+ for (let i = 0; i < data.length; i += 4) {
1508
+ data[i] = data[i] * 1.12 + 12;
1509
+ data[i + 1] = data[i + 1] * 1.04 + 5;
1510
+ data[i + 2] = data[i + 2] * 0.82;
1511
+ }
1512
+ }
1513
+ function coast(imageData) {
1514
+ const { data } = imageData;
1515
+ for (let i = 0; i < data.length; i += 4) {
1516
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1517
+ const shadow = Math.max(0, 1 - luma2 / 140);
1518
+ data[i] = (data[i] - 128) * 1.12 + 132;
1519
+ data[i + 1] = (data[i + 1] - 128) * 1.12 + 132 + 10 * shadow;
1520
+ data[i + 2] = (data[i + 2] - 128) * 1.12 + 132 + 22 * shadow;
1521
+ }
1522
+ }
1523
+ function mist(imageData) {
1524
+ const { data } = imageData;
1525
+ for (let i = 0; i < data.length; i += 4) {
1526
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1527
+ data[i] = 22 + (0.6 * data[i] + 0.4 * luma2) * 0.84;
1528
+ data[i + 1] = 22 + (0.6 * data[i + 1] + 0.4 * luma2) * 0.84;
1529
+ data[i + 2] = 26 + (0.6 * data[i + 2] + 0.4 * luma2) * 0.84;
1530
+ }
1531
+ }
1532
+ function berry(imageData) {
1533
+ const { data } = imageData;
1534
+ for (let i = 0; i < data.length; i += 4) {
1535
+ data[i] = (data[i] - 128) * 1.08 + 128 + 10;
1536
+ data[i + 1] = (data[i + 1] - 128) * 1.08 + 128 - 12;
1537
+ data[i + 2] = (data[i + 2] - 128) * 1.08 + 128 + 8;
1538
+ }
1539
+ }
1540
+ function cinema(imageData) {
1541
+ const { data } = imageData;
1542
+ for (let i = 0; i < data.length; i += 4) {
1543
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1544
+ const balance = (luma2 - 128) / 128;
1545
+ data[i] = (data[i] - 128) * 1.06 + 128 + 16 * balance;
1546
+ data[i + 1] = (data[i + 1] - 128) * 1.06 + 128;
1547
+ data[i + 2] = (data[i + 2] - 128) * 1.06 + 128 - 20 * balance;
1548
+ }
1549
+ }
1550
+ function luna(imageData) {
1551
+ const { data } = imageData;
1552
+ for (let i = 0; i < data.length; i += 4) {
1553
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1554
+ const value = 18 + luma2 * 0.9;
1555
+ data[i] = value;
1556
+ data[i + 1] = value;
1557
+ data[i + 2] = value + 4;
1558
+ }
1559
+ }
1560
+ function visibleRect(state, oriented) {
1561
+ return state.crop ?? { x: 0, y: 0, ...oriented };
1562
+ }
1563
+ function context2d(canvas) {
1564
+ const context = canvas.getContext("2d");
1565
+ if (context === null) {
1566
+ throw new Error("Canvas 2D context unavailable");
1567
+ }
1568
+ return context;
1569
+ }
1570
+ function supportsContextFilter(context) {
1571
+ if (context === null) {
1572
+ return false;
1573
+ }
1574
+ context.filter = "blur(1px)";
1575
+ return context.filter !== "none";
1576
+ }
1577
+ let contextFilterSupport = null;
1578
+ function contextFilterAvailable() {
1579
+ contextFilterSupport ??= supportsContextFilter(document.createElement("canvas").getContext("2d"));
1580
+ return contextFilterSupport;
1581
+ }
1582
+ function obfuscate(oriented, rect, style) {
1583
+ const strength = Math.max(4, Math.round(Math.min(oriented.width, oriented.height) / 40));
1584
+ const out = document.createElement("canvas");
1585
+ out.width = Math.max(1, Math.ceil(rect.width));
1586
+ out.height = Math.max(1, Math.ceil(rect.height));
1587
+ const context = context2d(out);
1588
+ if (style === "blur" && contextFilterAvailable()) {
1589
+ const pad = strength * 2;
1590
+ context.filter = `blur(${strength}px)`;
1591
+ context.drawImage(
1592
+ oriented,
1593
+ rect.x - pad,
1594
+ rect.y - pad,
1595
+ rect.width + pad * 2,
1596
+ rect.height + pad * 2,
1597
+ -pad,
1598
+ -pad,
1599
+ out.width + pad * 2,
1600
+ out.height + pad * 2
1601
+ );
1602
+ return out;
1603
+ }
1604
+ const small = document.createElement("canvas");
1605
+ small.width = Math.max(1, Math.ceil(rect.width / strength));
1606
+ small.height = Math.max(1, Math.ceil(rect.height / strength));
1607
+ context2d(small).drawImage(oriented, rect.x, rect.y, rect.width, rect.height, 0, 0, small.width, small.height);
1608
+ context.imageSmoothingEnabled = false;
1609
+ context.drawImage(small, 0, 0, out.width, out.height);
1610
+ return out;
1611
+ }
1612
+ function buildAnnotationNode(annotation, oriented) {
1613
+ const base = { id: annotation.id, name: "annotation" };
1614
+ switch (annotation.type) {
1615
+ case "draw":
1616
+ return new Konva__default.default.Line({
1617
+ ...base,
1618
+ points: annotation.points,
1619
+ stroke: annotation.color,
1620
+ strokeWidth: annotation.strokeWidth,
1621
+ lineCap: "round",
1622
+ lineJoin: "round"
1623
+ });
1624
+ case "arrow":
1625
+ return new Konva__default.default.Arrow({
1626
+ ...base,
1627
+ points: [...annotation.points],
1628
+ stroke: annotation.color,
1629
+ fill: annotation.color,
1630
+ strokeWidth: annotation.strokeWidth,
1631
+ pointerLength: annotation.strokeWidth * 4,
1632
+ pointerWidth: annotation.strokeWidth * 4
1633
+ });
1634
+ case "rectangle":
1635
+ return new Konva__default.default.Rect({
1636
+ ...base,
1637
+ ...annotation.rect,
1638
+ rotation: annotation.rotation,
1639
+ stroke: annotation.color,
1640
+ strokeWidth: annotation.strokeWidth
1641
+ });
1642
+ case "ellipse":
1643
+ return new Konva__default.default.Ellipse({
1644
+ ...base,
1645
+ x: annotation.rect.x,
1646
+ y: annotation.rect.y,
1647
+ offsetX: -annotation.rect.width / 2,
1648
+ offsetY: -annotation.rect.height / 2,
1649
+ radiusX: annotation.rect.width / 2,
1650
+ radiusY: annotation.rect.height / 2,
1651
+ rotation: annotation.rotation,
1652
+ stroke: annotation.color,
1653
+ strokeWidth: annotation.strokeWidth
1654
+ });
1655
+ case "text":
1656
+ case "sticker":
1657
+ return new Konva__default.default.Text({
1658
+ ...base,
1659
+ x: annotation.x,
1660
+ y: annotation.y,
1661
+ text: annotation.text,
1662
+ fill: annotation.color,
1663
+ fontSize: annotation.fontSize,
1664
+ rotation: annotation.rotation,
1665
+ // Kept in sync with the text overlay for WYSIWYG editing
1666
+ fontFamily: "Helvetica, Arial, sans-serif"
1667
+ });
1668
+ case "redact": {
1669
+ if (oriented === void 0) {
1670
+ throw new Error("Redaction requires the oriented image");
1671
+ }
1672
+ return new Konva__default.default.Image({
1673
+ ...base,
1674
+ image: obfuscate(oriented, annotation.rect, annotation.style),
1675
+ x: annotation.rect.x,
1676
+ y: annotation.rect.y
1677
+ });
1678
+ }
1679
+ }
1680
+ }
1681
+ function applyFilters(node, state, pixelRatio = 1) {
1682
+ const { brightness, contrast, saturation } = state.adjustments;
1683
+ const filters = [];
1684
+ if (brightness !== 0) {
1685
+ filters.push(Konva__default.default.Filters.Brighten);
1686
+ }
1687
+ if (contrast !== 0) {
1688
+ filters.push(Konva__default.default.Filters.Contrast);
1689
+ }
1690
+ if (saturation !== 0) {
1691
+ filters.push(saturate);
1692
+ }
1693
+ const presetFilters = {
1694
+ none: null,
1695
+ grayscale: Konva__default.default.Filters.Grayscale,
1696
+ noir,
1697
+ luna,
1698
+ sepia: Konva__default.default.Filters.Sepia,
1699
+ fade,
1700
+ warm,
1701
+ cool,
1702
+ golden,
1703
+ coast,
1704
+ mist,
1705
+ berry,
1706
+ cinema,
1707
+ invert: Konva__default.default.Filters.Invert,
1708
+ solarize: Konva__default.default.Filters.Solarize,
1709
+ posterize: Konva__default.default.Filters.Posterize,
1710
+ pop: Konva__default.default.Filters.Enhance
1711
+ }[state.preset];
1712
+ if (presetFilters !== null) {
1713
+ filters.push(presetFilters);
1714
+ }
1715
+ if (filters.length === 0) {
1716
+ node.filters([]);
1717
+ node.clearCache();
1718
+ return;
1719
+ }
1720
+ node.filters(filters);
1721
+ node.brightness(brightness / 100);
1722
+ node.contrast(contrast);
1723
+ node.saturation(saturation / 100);
1724
+ if (state.preset === "posterize") {
1725
+ node.levels(0.02);
1726
+ }
1727
+ if (state.preset === "pop") {
1728
+ node.enhance(0.25);
1729
+ }
1730
+ node.cache({ pixelRatio });
1731
+ }
1732
+ function thumbnailKey(state) {
1733
+ const { crop, adjustments } = state;
1734
+ return [
1735
+ crop?.x,
1736
+ crop?.y,
1737
+ crop?.width,
1738
+ crop?.height,
1739
+ adjustments.brightness,
1740
+ adjustments.contrast,
1741
+ adjustments.saturation
1742
+ ].join("|");
1743
+ }
1744
+ function presetThumbnail(oriented, state, preset, size = 96) {
1745
+ const visible = visibleRect(state, { width: oriented.width, height: oriented.height });
1746
+ const scale = Math.min(size / visible.width, size / visible.height);
1747
+ const thumb = document.createElement("canvas");
1748
+ thumb.width = Math.max(1, Math.round(visible.width * scale));
1749
+ thumb.height = Math.max(1, Math.round(visible.height * scale));
1750
+ context2d(thumb).drawImage(oriented, visible.x, visible.y, visible.width, visible.height, 0, 0, thumb.width, thumb.height);
1751
+ const stage = new Konva__default.default.Stage({
1752
+ container: document.createElement("div"),
1753
+ width: thumb.width,
1754
+ height: thumb.height
1755
+ });
1756
+ try {
1757
+ const node = new Konva__default.default.Image({ image: thumb, listening: false });
1758
+ applyFilters(node, { ...state, preset });
1759
+ const layer = new Konva__default.default.Layer();
1760
+ layer.add(node);
1761
+ stage.add(layer);
1762
+ return stage.toDataURL();
1763
+ } finally {
1764
+ stage.destroy();
1765
+ }
1766
+ }
1767
+ function createScene(stage) {
1768
+ const layer = new Konva__default.default.Layer();
1769
+ const viewGroup = new Konva__default.default.Group({ name: "view" });
1770
+ const contentGroup = new Konva__default.default.Group({ name: "content" });
1771
+ const imageNode = new Konva__default.default.Image({ image: void 0, listening: false });
1772
+ contentGroup.add(imageNode);
1773
+ viewGroup.add(contentGroup);
1774
+ layer.add(viewGroup);
1775
+ stage.add(layer);
1776
+ const built = /* @__PURE__ */ new Map();
1777
+ let filterKey = "";
1778
+ const update = (oriented, state, options) => {
1779
+ const orientedChanged = imageNode.image() !== oriented;
1780
+ if (orientedChanged) {
1781
+ imageNode.image(oriented);
1782
+ }
1783
+ const origin = options.showCropped ? visibleRect(state, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
1784
+ viewGroup.position({
1785
+ x: options.offset.x - origin.x * options.scale,
1786
+ y: options.offset.y - origin.y * options.scale
1787
+ });
1788
+ viewGroup.scale({ x: options.scale, y: options.scale });
1789
+ if (options.showCropped && state.crop !== null) {
1790
+ viewGroup.clip(state.crop);
1791
+ } else {
1792
+ viewGroup.clipWidth(void 0);
1793
+ viewGroup.clipHeight(void 0);
1794
+ }
1795
+ const pixelRatio = options.fastFilters ? Math.min(1, options.scale * (globalThis.devicePixelRatio || 1)) : 1;
1796
+ const { brightness, contrast, saturation } = state.adjustments;
1797
+ const nextFilterKey = `${brightness}|${contrast}|${saturation}|${state.preset}|${pixelRatio}`;
1798
+ if (orientedChanged || nextFilterKey !== filterKey) {
1799
+ applyFilters(imageNode, state, pixelRatio);
1800
+ filterKey = nextFilterKey;
1801
+ }
1802
+ const seen = /* @__PURE__ */ new Set();
1803
+ for (const annotation of state.annotations) {
1804
+ seen.add(annotation.id);
1805
+ const entry = built.get(annotation.id);
1806
+ if (entry !== void 0 && entry.annotation === annotation && !orientedChanged) {
1807
+ continue;
1808
+ }
1809
+ entry?.node.destroy();
1810
+ const node = buildAnnotationNode(annotation, oriented);
1811
+ contentGroup.add(node);
1812
+ built.set(annotation.id, { annotation, node });
1813
+ }
1814
+ for (const [id, entry] of built) {
1815
+ if (!seen.has(id)) {
1816
+ entry.node.destroy();
1817
+ built.delete(id);
1818
+ }
1819
+ }
1820
+ imageNode.zIndex(0);
1821
+ state.annotations.forEach((annotation, index) => built.get(annotation.id).node.zIndex(index + 1));
1822
+ };
1823
+ return {
1824
+ viewGroup,
1825
+ contentGroup,
1826
+ imageNode,
1827
+ update,
1828
+ destroy: () => {
1829
+ built.clear();
1830
+ layer.destroy();
1831
+ }
1832
+ };
1833
+ }
1834
+ function renderScene(stage, oriented, state, options) {
1835
+ const scene = createScene(stage);
1836
+ scene.update(oriented, state, options);
1837
+ return scene;
1838
+ }
1839
+ function renderToCanvas(oriented, state, maxSize) {
1840
+ const visible = visibleRect(state, { width: oriented.width, height: oriented.height });
1841
+ const pixelRatio = maxSize === void 0 ? 1 : Math.min(1, maxSize / Math.max(visible.width, visible.height));
1842
+ const stage = new Konva__default.default.Stage({
1843
+ // Detached container: the export stage is never displayed
1844
+ container: document.createElement("div"),
1845
+ width: visible.width,
1846
+ height: visible.height
1847
+ });
1848
+ try {
1849
+ renderScene(stage, oriented, state, {
1850
+ scale: 1,
1851
+ offset: { x: 0, y: 0 },
1852
+ showCropped: true
1853
+ });
1854
+ return stage.toCanvas({ pixelRatio });
1855
+ } finally {
1856
+ stage.destroy();
1857
+ }
1858
+ }
1859
+ function toImageCoords(pointer, state, oriented, options) {
1860
+ const visible = visibleRect(state, oriented);
1861
+ return {
1862
+ x: (pointer.x - options.offset.x) / options.scale + (options.showCropped ? visible.x : 0),
1863
+ y: (pointer.y - options.offset.y) / options.scale + (options.showCropped ? visible.y : 0)
1864
+ };
1865
+ }
1866
+ const _sfc_main$p = /* @__PURE__ */ vue.defineComponent({
1867
+ __name: "FilterStrip",
1868
+ props: {
1869
+ loaded: { type: Boolean },
1870
+ oriented: {}
1871
+ },
1872
+ setup(__props) {
1873
+ const props = __props;
1874
+ const context = useEditorContext();
1875
+ const presets = [
1876
+ { id: "none", label: t("No filter") },
1877
+ { id: "pop", label: t("Pop") },
1878
+ { id: "golden", label: t("Golden") },
1879
+ { id: "coast", label: t("Coast") },
1880
+ { id: "cinema", label: t("Cinema") },
1881
+ { id: "berry", label: t("Berry") },
1882
+ { id: "mist", label: t("Mist") },
1883
+ { id: "warm", label: t("Warm") },
1884
+ { id: "cool", label: t("Cool") },
1885
+ { id: "fade", label: t("Fade") },
1886
+ { id: "grayscale", label: t("Grayscale") },
1887
+ { id: "noir", label: t("Noir") },
1888
+ { id: "luna", label: t("Luna") },
1889
+ { id: "sepia", label: t("Sepia") },
1890
+ { id: "invert", label: t("Invert") },
1891
+ { id: "solarize", label: t("Solarize") },
1892
+ { id: "posterize", label: t("Posterize") }
1893
+ ];
1894
+ const presetPreviews = vue.shallowRef([]);
1895
+ vue.watch(
1896
+ [() => props.oriented, () => thumbnailKey(context.state.value)],
1897
+ ([oriented]) => {
1898
+ presetPreviews.value = oriented ? presets.map((preset) => ({
1899
+ ...preset,
1900
+ url: presetThumbnail(oriented, context.state.value, preset.id)
1901
+ })) : [];
1902
+ },
1903
+ { immediate: true }
1904
+ );
1905
+ function setPreset(preset, label) {
1906
+ context.commit({ ...context.state.value, preset }, label);
1907
+ }
1908
+ return (_ctx, _cache) => {
1909
+ return vue.openBlock(), vue.createBlock(GlassSurface, {
1910
+ variant: "strip",
1911
+ class: "filter-strip"
1912
+ }, {
1913
+ default: vue.withCtx(() => [
1914
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(presetPreviews.value, (preset) => {
1915
+ return vue.openBlock(), vue.createBlock(PresetChip, {
1916
+ key: preset.id,
1917
+ url: preset.url,
1918
+ label: preset.label,
1919
+ active: vue.unref(context).state.value.preset === preset.id,
1920
+ disabled: !__props.loaded,
1921
+ "data-test": `preset-${preset.id}`,
1922
+ onClick: ($event) => setPreset(preset.id, preset.label)
1923
+ }, null, 8, ["url", "label", "active", "disabled", "data-test", "onClick"]);
1924
+ }), 128))
1925
+ ]),
1926
+ _: 1
1927
+ });
1928
+ };
1929
+ }
1930
+ });
1931
+ const FilterStrip = /* @__PURE__ */ _export_sfc(_sfc_main$p, [["__scopeId", "data-v-44007efa"]]);
1932
+ const _hoisted_1$m = { class: "redact-panel" };
1933
+ const _sfc_main$o = /* @__PURE__ */ vue.defineComponent({
1934
+ __name: "RedactPanel",
1935
+ props: {
1936
+ loaded: { type: Boolean }
1937
+ },
1938
+ setup(__props) {
1939
+ const context = useEditorContext();
1940
+ const labels = {
1941
+ pixelate: t("Pixelate"),
1942
+ blur: t("Blur")
1943
+ };
1944
+ return (_ctx, _cache) => {
1945
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$m, [
1946
+ vue.createVNode(vue.unref(NcButton__default.default), {
1947
+ "data-test": "redact-pixelate",
1948
+ pressed: vue.unref(context).redactStyle.value === "pixelate",
1949
+ disabled: !__props.loaded,
1950
+ variant: "tertiary",
1951
+ onClick: _cache[0] || (_cache[0] = ($event) => vue.unref(context).redactStyle.value = "pixelate")
1952
+ }, {
1953
+ default: vue.withCtx(() => [
1954
+ vue.createTextVNode(vue.toDisplayString(labels.pixelate), 1)
1955
+ ]),
1956
+ _: 1
1957
+ }, 8, ["pressed", "disabled"]),
1958
+ vue.createVNode(vue.unref(NcButton__default.default), {
1959
+ "data-test": "redact-blur",
1960
+ pressed: vue.unref(context).redactStyle.value === "blur",
1961
+ disabled: !__props.loaded,
1962
+ variant: "tertiary",
1963
+ onClick: _cache[1] || (_cache[1] = ($event) => vue.unref(context).redactStyle.value = "blur")
1964
+ }, {
1965
+ default: vue.withCtx(() => [
1966
+ vue.createTextVNode(vue.toDisplayString(labels.blur), 1)
1967
+ ]),
1968
+ _: 1
1969
+ }, 8, ["pressed", "disabled"])
1970
+ ]);
1971
+ };
1972
+ }
1973
+ });
1974
+ const RedactPanel = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-ac701267"]]);
1975
+ const _hoisted_1$l = { class: "select-panel" };
1976
+ const _hoisted_2$i = {
1977
+ key: 0,
1978
+ class: "select-panel__option"
1979
+ };
1980
+ const _hoisted_3$i = ["value", "disabled"];
1981
+ const _hoisted_4$i = {
1982
+ key: 1,
1983
+ class: "select-panel__hint"
1984
+ };
1985
+ const _hoisted_5$1 = {
1986
+ key: 2,
1987
+ class: "select-panel__hint"
1988
+ };
1989
+ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
1990
+ __name: "SelectPanel",
1991
+ props: {
1992
+ loaded: { type: Boolean }
1993
+ },
1994
+ setup(__props) {
1995
+ const context = useEditorContext();
1996
+ const color = useAnnotationColor(context);
1997
+ const colorLabel = t("Color");
1998
+ const hint = t("Click an annotation to move, resize or recolor it");
1999
+ const transformHint = t("Drag to move, use the handles to resize");
2000
+ const selectedAnnotation = vue.computed(() => context.state.value.annotations.find((annotation) => annotation.id === context.selectedId.value) ?? null);
2001
+ const recolorable = vue.computed(() => selectedAnnotation.value !== null && selectedAnnotation.value.type !== "sticker" && selectedAnnotation.value.type !== "redact");
2002
+ return (_ctx, _cache) => {
2003
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$l, [
2004
+ recolorable.value ? (vue.openBlock(), vue.createElementBlock("label", _hoisted_2$i, [
2005
+ vue.createTextVNode(vue.toDisplayString(vue.unref(colorLabel)) + " ", 1),
2006
+ vue.createElementVNode("input", {
2007
+ value: vue.unref(context).drawColor.value,
2008
+ type: "color",
2009
+ disabled: !__props.loaded,
2010
+ "data-test": "color",
2011
+ onInput: _cache[0] || (_cache[0] = ($event) => vue.unref(color).preview($event.target.value)),
2012
+ onChange: _cache[1] || (_cache[1] = ($event) => vue.unref(color).commit($event.target.value))
2013
+ }, null, 40, _hoisted_3$i)
2014
+ ])) : selectedAnnotation.value !== null ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4$i, vue.toDisplayString(vue.unref(transformHint)), 1)) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_5$1, vue.toDisplayString(vue.unref(hint)), 1))
2015
+ ]);
2016
+ };
2017
+ }
2018
+ });
2019
+ const SelectPanel = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-2ca9996e"]]);
2020
+ const _hoisted_1$k = { class: "sticker-panel" };
2021
+ const _sfc_main$m = /* @__PURE__ */ vue.defineComponent({
2022
+ __name: "StickerPanel",
2023
+ props: {
2024
+ loaded: { type: Boolean }
2025
+ },
2026
+ setup(__props) {
2027
+ const context = useEditorContext();
2028
+ const moreLabel = t("More emojis");
2029
+ const FALLBACK_STICKERS = ["😀", "😍", "🎉", "👍", "❤️", "⭐", "🔥", "💡", "✅", "❌", "❓", "⚠️"];
2030
+ function frequentStickers() {
2031
+ try {
2032
+ const frequent = emoji.emojiSearch("", 12).map((emoji2) => emoji2.native).filter((native) => typeof native === "string" && native !== "");
2033
+ return frequent.length >= 6 ? frequent : FALLBACK_STICKERS;
2034
+ } catch {
2035
+ return FALLBACK_STICKERS;
2036
+ }
2037
+ }
2038
+ const DEFAULT_STICKERS = frequentStickers();
2039
+ const stickers = vue.computed(() => DEFAULT_STICKERS.includes(context.sticker.value) ? DEFAULT_STICKERS : [context.sticker.value, ...DEFAULT_STICKERS.slice(0, 11)]);
2040
+ return (_ctx, _cache) => {
2041
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$k, [
2042
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(stickers.value, (sticker) => {
2043
+ return vue.openBlock(), vue.createBlock(vue.unref(NcButton__default.default), {
2044
+ key: sticker,
2045
+ "aria-label": sticker,
2046
+ pressed: vue.unref(context).sticker.value === sticker,
2047
+ disabled: !__props.loaded,
2048
+ class: "sticker-panel__emoji",
2049
+ variant: "tertiary",
2050
+ onClick: ($event) => vue.unref(context).sticker.value = sticker
2051
+ }, {
2052
+ default: vue.withCtx(() => [
2053
+ vue.createTextVNode(vue.toDisplayString(sticker), 1)
2054
+ ]),
2055
+ _: 2
2056
+ }, 1032, ["aria-label", "pressed", "disabled", "onClick"]);
2057
+ }), 128)),
2058
+ vue.createVNode(vue.unref(NcEmojiPicker__default.default), {
2059
+ onSelect: _cache[0] || (_cache[0] = ($event) => vue.unref(context).sticker.value = $event)
2060
+ }, {
2061
+ default: vue.withCtx(() => [
2062
+ vue.createVNode(vue.unref(NcButton__default.default), {
2063
+ "data-test": "emoji-picker",
2064
+ variant: "tertiary"
2065
+ }, {
2066
+ default: vue.withCtx(() => [
2067
+ vue.createTextVNode(vue.toDisplayString(vue.unref(moreLabel)), 1)
2068
+ ]),
2069
+ _: 1
2070
+ })
2071
+ ]),
2072
+ _: 1
2073
+ })
2074
+ ]);
2075
+ };
2076
+ }
2077
+ });
2078
+ const StickerPanel = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-6307599b"]]);
2079
+ const _sfc_main$l = /* @__PURE__ */ vue.defineComponent({
2080
+ __name: "EditorPanel",
2081
+ props: {
2082
+ loaded: { type: Boolean },
2083
+ oriented: {}
2084
+ },
2085
+ setup(__props) {
2086
+ const context = useEditorContext();
2087
+ return (_ctx, _cache) => {
2088
+ return vue.unref(context).activeMode.value === "filter" ? (vue.openBlock(), vue.createBlock(FilterStrip, {
2089
+ key: 0,
2090
+ loaded: __props.loaded,
2091
+ oriented: __props.oriented
2092
+ }, null, 8, ["loaded", "oriented"])) : (vue.openBlock(), vue.createBlock(GlassSurface, {
2093
+ key: 1,
2094
+ variant: "card",
2095
+ class: "editor-card"
2096
+ }, {
2097
+ default: vue.withCtx(() => [
2098
+ vue.unref(context).activeMode.value === "crop" ? (vue.openBlock(), vue.createBlock(CropPanel, {
2099
+ key: 0,
2100
+ loaded: __props.loaded
2101
+ }, null, 8, ["loaded"])) : vue.unref(context).activeMode.value === "finetune" ? (vue.openBlock(), vue.createBlock(AdjustPanel, {
2102
+ key: 1,
2103
+ loaded: __props.loaded
2104
+ }, null, 8, ["loaded"])) : vue.unref(context).activeMode.value === "annotate" ? (vue.openBlock(), vue.createBlock(AnnotatePanel, {
2105
+ key: 2,
2106
+ loaded: __props.loaded
2107
+ }, null, 8, ["loaded"])) : vue.unref(context).activeMode.value === "select" ? (vue.openBlock(), vue.createBlock(SelectPanel, {
2108
+ key: 3,
2109
+ loaded: __props.loaded
2110
+ }, null, 8, ["loaded"])) : vue.unref(context).activeMode.value === "sticker" ? (vue.openBlock(), vue.createBlock(StickerPanel, {
2111
+ key: 4,
2112
+ loaded: __props.loaded
2113
+ }, null, 8, ["loaded"])) : vue.unref(context).activeMode.value === "redact" ? (vue.openBlock(), vue.createBlock(RedactPanel, {
2114
+ key: 5,
2115
+ loaded: __props.loaded
2116
+ }, null, 8, ["loaded"])) : vue.createCommentVNode("", true)
2117
+ ]),
2118
+ _: 1
2119
+ }));
2120
+ };
2121
+ }
2122
+ });
2123
+ const EditorPanel = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-3fb85d8f"]]);
2124
+ const _sfc_main$k = {
2125
+ name: "BlurIcon",
2126
+ emits: ["click"],
2127
+ props: {
2128
+ title: {
2129
+ type: String
2130
+ },
2131
+ fillColor: {
2132
+ type: String,
2133
+ default: "currentColor"
2134
+ },
2135
+ size: {
2136
+ type: Number,
2137
+ default: 24
2138
+ }
2139
+ }
2140
+ };
2141
+ const _hoisted_1$j = ["aria-hidden", "aria-label"];
2142
+ const _hoisted_2$h = ["fill", "width", "height"];
2143
+ const _hoisted_3$h = { d: "M14,8.5A1.5,1.5 0 0,0 12.5,10A1.5,1.5 0 0,0 14,11.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 14,8.5M14,12.5A1.5,1.5 0 0,0 12.5,14A1.5,1.5 0 0,0 14,15.5A1.5,1.5 0 0,0 15.5,14A1.5,1.5 0 0,0 14,12.5M10,17A1,1 0 0,0 9,18A1,1 0 0,0 10,19A1,1 0 0,0 11,18A1,1 0 0,0 10,17M10,8.5A1.5,1.5 0 0,0 8.5,10A1.5,1.5 0 0,0 10,11.5A1.5,1.5 0 0,0 11.5,10A1.5,1.5 0 0,0 10,8.5M14,20.5A0.5,0.5 0 0,0 13.5,21A0.5,0.5 0 0,0 14,21.5A0.5,0.5 0 0,0 14.5,21A0.5,0.5 0 0,0 14,20.5M14,17A1,1 0 0,0 13,18A1,1 0 0,0 14,19A1,1 0 0,0 15,18A1,1 0 0,0 14,17M21,13.5A0.5,0.5 0 0,0 20.5,14A0.5,0.5 0 0,0 21,14.5A0.5,0.5 0 0,0 21.5,14A0.5,0.5 0 0,0 21,13.5M18,5A1,1 0 0,0 17,6A1,1 0 0,0 18,7A1,1 0 0,0 19,6A1,1 0 0,0 18,5M18,9A1,1 0 0,0 17,10A1,1 0 0,0 18,11A1,1 0 0,0 19,10A1,1 0 0,0 18,9M18,17A1,1 0 0,0 17,18A1,1 0 0,0 18,19A1,1 0 0,0 19,18A1,1 0 0,0 18,17M18,13A1,1 0 0,0 17,14A1,1 0 0,0 18,15A1,1 0 0,0 19,14A1,1 0 0,0 18,13M10,12.5A1.5,1.5 0 0,0 8.5,14A1.5,1.5 0 0,0 10,15.5A1.5,1.5 0 0,0 11.5,14A1.5,1.5 0 0,0 10,12.5M10,7A1,1 0 0,0 11,6A1,1 0 0,0 10,5A1,1 0 0,0 9,6A1,1 0 0,0 10,7M10,3.5A0.5,0.5 0 0,0 10.5,3A0.5,0.5 0 0,0 10,2.5A0.5,0.5 0 0,0 9.5,3A0.5,0.5 0 0,0 10,3.5M10,20.5A0.5,0.5 0 0,0 9.5,21A0.5,0.5 0 0,0 10,21.5A0.5,0.5 0 0,0 10.5,21A0.5,0.5 0 0,0 10,20.5M3,13.5A0.5,0.5 0 0,0 2.5,14A0.5,0.5 0 0,0 3,14.5A0.5,0.5 0 0,0 3.5,14A0.5,0.5 0 0,0 3,13.5M14,3.5A0.5,0.5 0 0,0 14.5,3A0.5,0.5 0 0,0 14,2.5A0.5,0.5 0 0,0 13.5,3A0.5,0.5 0 0,0 14,3.5M14,7A1,1 0 0,0 15,6A1,1 0 0,0 14,5A1,1 0 0,0 13,6A1,1 0 0,0 14,7M21,10.5A0.5,0.5 0 0,0 21.5,10A0.5,0.5 0 0,0 21,9.5A0.5,0.5 0 0,0 20.5,10A0.5,0.5 0 0,0 21,10.5M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5M3,9.5A0.5,0.5 0 0,0 2.5,10A0.5,0.5 0 0,0 3,10.5A0.5,0.5 0 0,0 3.5,10A0.5,0.5 0 0,0 3,9.5M6,9A1,1 0 0,0 5,10A1,1 0 0,0 6,11A1,1 0 0,0 7,10A1,1 0 0,0 6,9M6,17A1,1 0 0,0 5,18A1,1 0 0,0 6,19A1,1 0 0,0 7,18A1,1 0 0,0 6,17M6,13A1,1 0 0,0 5,14A1,1 0 0,0 6,15A1,1 0 0,0 7,14A1,1 0 0,0 6,13Z" };
2144
+ const _hoisted_4$h = { key: 0 };
2145
+ function _sfc_render$f(_ctx, _cache, $props, $setup, $data, $options) {
2146
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2147
+ "aria-hidden": $props.title ? null : "true",
2148
+ "aria-label": $props.title,
2149
+ class: "material-design-icon blur-icon",
2150
+ role: "img",
2151
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2152
+ }), [
2153
+ (vue.openBlock(), vue.createElementBlock("svg", {
2154
+ fill: $props.fillColor,
2155
+ class: "material-design-icon__svg",
2156
+ width: $props.size,
2157
+ height: $props.size,
2158
+ viewBox: "0 0 24 24"
2159
+ }, [
2160
+ vue.createElementVNode("path", _hoisted_3$h, [
2161
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$h, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2162
+ ])
2163
+ ], 8, _hoisted_2$h))
2164
+ ], 16, _hoisted_1$j);
2165
+ }
2166
+ const Blur = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["render", _sfc_render$f]]);
2167
+ const _sfc_main$j = {
2168
+ name: "CropIcon",
2169
+ emits: ["click"],
2170
+ props: {
2171
+ title: {
2172
+ type: String
2173
+ },
2174
+ fillColor: {
2175
+ type: String,
2176
+ default: "currentColor"
2177
+ },
2178
+ size: {
2179
+ type: Number,
2180
+ default: 24
2181
+ }
2182
+ }
2183
+ };
2184
+ const _hoisted_1$i = ["aria-hidden", "aria-label"];
2185
+ const _hoisted_2$g = ["fill", "width", "height"];
2186
+ const _hoisted_3$g = { d: "M7,17V1H5V5H1V7H5V17A2,2 0 0,0 7,19H17V23H19V19H23V17M17,15H19V7C19,5.89 18.1,5 17,5H9V7H17V15Z" };
2187
+ const _hoisted_4$g = { key: 0 };
2188
+ function _sfc_render$e(_ctx, _cache, $props, $setup, $data, $options) {
2189
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2190
+ "aria-hidden": $props.title ? null : "true",
2191
+ "aria-label": $props.title,
2192
+ class: "material-design-icon crop-icon",
2193
+ role: "img",
2194
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2195
+ }), [
2196
+ (vue.openBlock(), vue.createElementBlock("svg", {
2197
+ fill: $props.fillColor,
2198
+ class: "material-design-icon__svg",
2199
+ width: $props.size,
2200
+ height: $props.size,
2201
+ viewBox: "0 0 24 24"
2202
+ }, [
2203
+ vue.createElementVNode("path", _hoisted_3$g, [
2204
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$g, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2205
+ ])
2206
+ ], 8, _hoisted_2$g))
2207
+ ], 16, _hoisted_1$i);
2208
+ }
2209
+ const Crop = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["render", _sfc_render$e]]);
2210
+ const _sfc_main$i = {
2211
+ name: "CursorDefaultOutlineIcon",
2212
+ emits: ["click"],
2213
+ props: {
2214
+ title: {
2215
+ type: String
2216
+ },
2217
+ fillColor: {
2218
+ type: String,
2219
+ default: "currentColor"
2220
+ },
2221
+ size: {
2222
+ type: Number,
2223
+ default: 24
2224
+ }
2225
+ }
2226
+ };
2227
+ const _hoisted_1$h = ["aria-hidden", "aria-label"];
2228
+ const _hoisted_2$f = ["fill", "width", "height"];
2229
+ const _hoisted_3$f = { d: "M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" };
2230
+ const _hoisted_4$f = { key: 0 };
2231
+ function _sfc_render$d(_ctx, _cache, $props, $setup, $data, $options) {
2232
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2233
+ "aria-hidden": $props.title ? null : "true",
2234
+ "aria-label": $props.title,
2235
+ class: "material-design-icon cursor-default-outline-icon",
2236
+ role: "img",
2237
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2238
+ }), [
2239
+ (vue.openBlock(), vue.createElementBlock("svg", {
2240
+ fill: $props.fillColor,
2241
+ class: "material-design-icon__svg",
2242
+ width: $props.size,
2243
+ height: $props.size,
2244
+ viewBox: "0 0 24 24"
2245
+ }, [
2246
+ vue.createElementVNode("path", _hoisted_3$f, [
2247
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$f, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2248
+ ])
2249
+ ], 8, _hoisted_2$f))
2250
+ ], 16, _hoisted_1$h);
2251
+ }
2252
+ const CursorDefaultOutline = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["render", _sfc_render$d]]);
2253
+ const _sfc_main$h = {
2254
+ name: "PaletteOutlineIcon",
2255
+ emits: ["click"],
2256
+ props: {
2257
+ title: {
2258
+ type: String
2259
+ },
2260
+ fillColor: {
2261
+ type: String,
2262
+ default: "currentColor"
2263
+ },
2264
+ size: {
2265
+ type: Number,
2266
+ default: 24
2267
+ }
2268
+ }
2269
+ };
2270
+ const _hoisted_1$g = ["aria-hidden", "aria-label"];
2271
+ const _hoisted_2$e = ["fill", "width", "height"];
2272
+ const _hoisted_3$e = { d: "M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" };
2273
+ const _hoisted_4$e = { key: 0 };
2274
+ function _sfc_render$c(_ctx, _cache, $props, $setup, $data, $options) {
2275
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2276
+ "aria-hidden": $props.title ? null : "true",
2277
+ "aria-label": $props.title,
2278
+ class: "material-design-icon palette-outline-icon",
2279
+ role: "img",
2280
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2281
+ }), [
2282
+ (vue.openBlock(), vue.createElementBlock("svg", {
2283
+ fill: $props.fillColor,
2284
+ class: "material-design-icon__svg",
2285
+ width: $props.size,
2286
+ height: $props.size,
2287
+ viewBox: "0 0 24 24"
2288
+ }, [
2289
+ vue.createElementVNode("path", _hoisted_3$e, [
2290
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$e, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2291
+ ])
2292
+ ], 8, _hoisted_2$e))
2293
+ ], 16, _hoisted_1$g);
2294
+ }
2295
+ const PaletteOutline = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["render", _sfc_render$c]]);
2296
+ const _sfc_main$g = {
2297
+ name: "StickerEmojiIcon",
2298
+ emits: ["click"],
2299
+ props: {
2300
+ title: {
2301
+ type: String
2302
+ },
2303
+ fillColor: {
2304
+ type: String,
2305
+ default: "currentColor"
2306
+ },
2307
+ size: {
2308
+ type: Number,
2309
+ default: 24
2310
+ }
2311
+ }
2312
+ };
2313
+ const _hoisted_1$f = ["aria-hidden", "aria-label"];
2314
+ const _hoisted_2$d = ["fill", "width", "height"];
2315
+ const _hoisted_3$d = { d: "M5.5,2C3.56,2 2,3.56 2,5.5V18.5C2,20.44 3.56,22 5.5,22H16L22,16V5.5C22,3.56 20.44,2 18.5,2H5.5M5.75,4H18.25A1.75,1.75 0 0,1 20,5.75V15H18.5C16.56,15 15,16.56 15,18.5V20H5.75A1.75,1.75 0 0,1 4,18.25V5.75A1.75,1.75 0 0,1 5.75,4M14.44,6.77C14.28,6.77 14.12,6.79 13.97,6.83C13.03,7.09 12.5,8.05 12.74,9C12.79,9.15 12.86,9.3 12.95,9.44L16.18,8.56C16.18,8.39 16.16,8.22 16.12,8.05C15.91,7.3 15.22,6.77 14.44,6.77M8.17,8.5C8,8.5 7.85,8.5 7.7,8.55C6.77,8.81 6.22,9.77 6.47,10.7C6.5,10.86 6.59,11 6.68,11.16L9.91,10.28C9.91,10.11 9.89,9.94 9.85,9.78C9.64,9 8.95,8.5 8.17,8.5M16.72,11.26L7.59,13.77C8.91,15.3 11,15.94 12.95,15.41C14.9,14.87 16.36,13.25 16.72,11.26Z" };
2316
+ const _hoisted_4$d = { key: 0 };
2317
+ function _sfc_render$b(_ctx, _cache, $props, $setup, $data, $options) {
2318
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2319
+ "aria-hidden": $props.title ? null : "true",
2320
+ "aria-label": $props.title,
2321
+ class: "material-design-icon sticker-emoji-icon",
2322
+ role: "img",
2323
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2324
+ }), [
2325
+ (vue.openBlock(), vue.createElementBlock("svg", {
2326
+ fill: $props.fillColor,
2327
+ class: "material-design-icon__svg",
2328
+ width: $props.size,
2329
+ height: $props.size,
2330
+ viewBox: "0 0 24 24"
2331
+ }, [
2332
+ vue.createElementVNode("path", _hoisted_3$d, [
2333
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$d, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2334
+ ])
2335
+ ], 8, _hoisted_2$d))
2336
+ ], 16, _hoisted_1$f);
2337
+ }
2338
+ const StickerEmoji = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["render", _sfc_render$b]]);
2339
+ const _sfc_main$f = {
2340
+ name: "TuneIcon",
2341
+ emits: ["click"],
2342
+ props: {
2343
+ title: {
2344
+ type: String
2345
+ },
2346
+ fillColor: {
2347
+ type: String,
2348
+ default: "currentColor"
2349
+ },
2350
+ size: {
2351
+ type: Number,
2352
+ default: 24
2353
+ }
2354
+ }
2355
+ };
2356
+ const _hoisted_1$e = ["aria-hidden", "aria-label"];
2357
+ const _hoisted_2$c = ["fill", "width", "height"];
2358
+ const _hoisted_3$c = { d: "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z" };
2359
+ const _hoisted_4$c = { key: 0 };
2360
+ function _sfc_render$a(_ctx, _cache, $props, $setup, $data, $options) {
2361
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2362
+ "aria-hidden": $props.title ? null : "true",
2363
+ "aria-label": $props.title,
2364
+ class: "material-design-icon tune-icon",
2365
+ role: "img",
2366
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2367
+ }), [
2368
+ (vue.openBlock(), vue.createElementBlock("svg", {
2369
+ fill: $props.fillColor,
2370
+ class: "material-design-icon__svg",
2371
+ width: $props.size,
2372
+ height: $props.size,
2373
+ viewBox: "0 0 24 24"
2374
+ }, [
2375
+ vue.createElementVNode("path", _hoisted_3$c, [
2376
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$c, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2377
+ ])
2378
+ ], 8, _hoisted_2$c))
2379
+ ], 16, _hoisted_1$e);
2380
+ }
2381
+ const Tune = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["render", _sfc_render$a]]);
2382
+ const _hoisted_1$d = { class: "editor-sidebar" };
2383
+ const _sfc_main$e = /* @__PURE__ */ vue.defineComponent({
2384
+ __name: "EditorSidebar",
2385
+ props: {
2386
+ loaded: { type: Boolean }
2387
+ },
2388
+ setup(__props) {
2389
+ const context = useEditorContext();
2390
+ const modes = [
2391
+ { id: "select", label: t("Select"), icon: CursorDefaultOutline },
2392
+ { id: "crop", label: t("Crop"), icon: Crop },
2393
+ { id: "finetune", label: t("Adjust"), icon: Tune },
2394
+ { id: "filter", label: t("Filter"), icon: PaletteOutline },
2395
+ { id: "annotate", label: t("Annotate"), icon: Pencil },
2396
+ { id: "sticker", label: t("Sticker"), icon: StickerEmoji },
2397
+ { id: "redact", label: t("Redact"), icon: Blur }
2398
+ ];
2399
+ return (_ctx, _cache) => {
2400
+ return vue.openBlock(), vue.createElementBlock("nav", _hoisted_1$d, [
2401
+ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(modes, (mode) => {
2402
+ return vue.createVNode(IconTab, {
2403
+ key: mode.id,
2404
+ label: mode.label,
2405
+ active: vue.unref(context).activeMode.value === mode.id,
2406
+ disabled: !__props.loaded,
2407
+ onClick: ($event) => vue.unref(context).setMode(mode.id)
2408
+ }, {
2409
+ default: vue.withCtx(() => [
2410
+ (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(mode.icon), { size: 20 }))
2411
+ ]),
2412
+ _: 2
2413
+ }, 1032, ["label", "active", "disabled", "onClick"]);
2414
+ }), 64))
2415
+ ]);
2416
+ };
2417
+ }
2418
+ });
2419
+ const EditorSidebar = /* @__PURE__ */ _export_sfc(_sfc_main$e, [["__scopeId", "data-v-a4223674"]]);
2420
+ const _sfc_main$d = {
2421
+ name: "CheckIcon",
2422
+ emits: ["click"],
2423
+ props: {
2424
+ title: {
2425
+ type: String
2426
+ },
2427
+ fillColor: {
2428
+ type: String,
2429
+ default: "currentColor"
2430
+ },
2431
+ size: {
2432
+ type: Number,
2433
+ default: 24
2434
+ }
2435
+ }
2436
+ };
2437
+ const _hoisted_1$c = ["aria-hidden", "aria-label"];
2438
+ const _hoisted_2$b = ["fill", "width", "height"];
2439
+ const _hoisted_3$b = { d: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" };
2440
+ const _hoisted_4$b = { key: 0 };
2441
+ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
2442
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2443
+ "aria-hidden": $props.title ? null : "true",
2444
+ "aria-label": $props.title,
2445
+ class: "material-design-icon check-icon",
2446
+ role: "img",
2447
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2448
+ }), [
2449
+ (vue.openBlock(), vue.createElementBlock("svg", {
2450
+ fill: $props.fillColor,
2451
+ class: "material-design-icon__svg",
2452
+ width: $props.size,
2453
+ height: $props.size,
2454
+ viewBox: "0 0 24 24"
2455
+ }, [
2456
+ vue.createElementVNode("path", _hoisted_3$b, [
2457
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$b, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2458
+ ])
2459
+ ], 8, _hoisted_2$b))
2460
+ ], 16, _hoisted_1$c);
2461
+ }
2462
+ const Check = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["render", _sfc_render$9]]);
2463
+ const _sfc_main$c = {
2464
+ name: "CloseIcon",
2465
+ emits: ["click"],
2466
+ props: {
2467
+ title: {
2468
+ type: String
2469
+ },
2470
+ fillColor: {
2471
+ type: String,
2472
+ default: "currentColor"
2473
+ },
2474
+ size: {
2475
+ type: Number,
2476
+ default: 24
2477
+ }
2478
+ }
2479
+ };
2480
+ const _hoisted_1$b = ["aria-hidden", "aria-label"];
2481
+ const _hoisted_2$a = ["fill", "width", "height"];
2482
+ const _hoisted_3$a = { d: "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" };
2483
+ const _hoisted_4$a = { key: 0 };
2484
+ function _sfc_render$8(_ctx, _cache, $props, $setup, $data, $options) {
2485
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2486
+ "aria-hidden": $props.title ? null : "true",
2487
+ "aria-label": $props.title,
2488
+ class: "material-design-icon close-icon",
2489
+ role: "img",
2490
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2491
+ }), [
2492
+ (vue.openBlock(), vue.createElementBlock("svg", {
2493
+ fill: $props.fillColor,
2494
+ class: "material-design-icon__svg",
2495
+ width: $props.size,
2496
+ height: $props.size,
2497
+ viewBox: "0 0 24 24"
2498
+ }, [
2499
+ vue.createElementVNode("path", _hoisted_3$a, [
2500
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$a, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2501
+ ])
2502
+ ], 8, _hoisted_2$a))
2503
+ ], 16, _hoisted_1$b);
2504
+ }
2505
+ const Close = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["render", _sfc_render$8]]);
2506
+ const _sfc_main$b = {
2507
+ name: "HistoryIcon",
2508
+ emits: ["click"],
2509
+ props: {
2510
+ title: {
2511
+ type: String
2512
+ },
2513
+ fillColor: {
2514
+ type: String,
2515
+ default: "currentColor"
2516
+ },
2517
+ size: {
2518
+ type: Number,
2519
+ default: 24
2520
+ }
2521
+ }
2522
+ };
2523
+ const _hoisted_1$a = ["aria-hidden", "aria-label"];
2524
+ const _hoisted_2$9 = ["fill", "width", "height"];
2525
+ const _hoisted_3$9 = { d: "M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" };
2526
+ const _hoisted_4$9 = { key: 0 };
2527
+ function _sfc_render$7(_ctx, _cache, $props, $setup, $data, $options) {
2528
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2529
+ "aria-hidden": $props.title ? null : "true",
2530
+ "aria-label": $props.title,
2531
+ class: "material-design-icon history-icon",
2532
+ role: "img",
2533
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2534
+ }), [
2535
+ (vue.openBlock(), vue.createElementBlock("svg", {
2536
+ fill: $props.fillColor,
2537
+ class: "material-design-icon__svg",
2538
+ width: $props.size,
2539
+ height: $props.size,
2540
+ viewBox: "0 0 24 24"
2541
+ }, [
2542
+ vue.createElementVNode("path", _hoisted_3$9, [
2543
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$9, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2544
+ ])
2545
+ ], 8, _hoisted_2$9))
2546
+ ], 16, _hoisted_1$a);
2547
+ }
2548
+ const HistoryIcon = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["render", _sfc_render$7]]);
2549
+ const _sfc_main$a = {
2550
+ name: "MagnifyMinusOutlineIcon",
2551
+ emits: ["click"],
2552
+ props: {
2553
+ title: {
2554
+ type: String
2555
+ },
2556
+ fillColor: {
2557
+ type: String,
2558
+ default: "currentColor"
2559
+ },
2560
+ size: {
2561
+ type: Number,
2562
+ default: 24
2563
+ }
2564
+ }
2565
+ };
2566
+ const _hoisted_1$9 = ["aria-hidden", "aria-label"];
2567
+ const _hoisted_2$8 = ["fill", "width", "height"];
2568
+ const _hoisted_3$8 = { d: "M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5A6.5,6.5 0 0,0 9.5,3A6.5,6.5 0 0,0 3,9.5A6.5,6.5 0 0,0 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.5L20.5,19L15.5,14M9.5,14C7,14 5,12 5,9.5C5,7 7,5 9.5,5C12,5 14,7 14,9.5C14,12 12,14 9.5,14M7,9H12V10H7V9Z" };
2569
+ const _hoisted_4$8 = { key: 0 };
2570
+ function _sfc_render$6(_ctx, _cache, $props, $setup, $data, $options) {
2571
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2572
+ "aria-hidden": $props.title ? null : "true",
2573
+ "aria-label": $props.title,
2574
+ class: "material-design-icon magnify-minus-outline-icon",
2575
+ role: "img",
2576
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2577
+ }), [
2578
+ (vue.openBlock(), vue.createElementBlock("svg", {
2579
+ fill: $props.fillColor,
2580
+ class: "material-design-icon__svg",
2581
+ width: $props.size,
2582
+ height: $props.size,
2583
+ viewBox: "0 0 24 24"
2584
+ }, [
2585
+ vue.createElementVNode("path", _hoisted_3$8, [
2586
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$8, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2587
+ ])
2588
+ ], 8, _hoisted_2$8))
2589
+ ], 16, _hoisted_1$9);
2590
+ }
2591
+ const MagnifyMinusOutline = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["render", _sfc_render$6]]);
2592
+ const _sfc_main$9 = {
2593
+ name: "MagnifyPlusOutlineIcon",
2594
+ emits: ["click"],
2595
+ props: {
2596
+ title: {
2597
+ type: String
2598
+ },
2599
+ fillColor: {
2600
+ type: String,
2601
+ default: "currentColor"
2602
+ },
2603
+ size: {
2604
+ type: Number,
2605
+ default: 24
2606
+ }
2607
+ }
2608
+ };
2609
+ const _hoisted_1$8 = ["aria-hidden", "aria-label"];
2610
+ const _hoisted_2$7 = ["fill", "width", "height"];
2611
+ const _hoisted_3$7 = { d: "M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z" };
2612
+ const _hoisted_4$7 = { key: 0 };
2613
+ function _sfc_render$5(_ctx, _cache, $props, $setup, $data, $options) {
2614
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2615
+ "aria-hidden": $props.title ? null : "true",
2616
+ "aria-label": $props.title,
2617
+ class: "material-design-icon magnify-plus-outline-icon",
2618
+ role: "img",
2619
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2620
+ }), [
2621
+ (vue.openBlock(), vue.createElementBlock("svg", {
2622
+ fill: $props.fillColor,
2623
+ class: "material-design-icon__svg",
2624
+ width: $props.size,
2625
+ height: $props.size,
2626
+ viewBox: "0 0 24 24"
2627
+ }, [
2628
+ vue.createElementVNode("path", _hoisted_3$7, [
2629
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$7, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2630
+ ])
2631
+ ], 8, _hoisted_2$7))
2632
+ ], 16, _hoisted_1$8);
2633
+ }
2634
+ const MagnifyPlusOutline = /* @__PURE__ */ _export_sfc(_sfc_main$9, [["render", _sfc_render$5]]);
2635
+ const _sfc_main$8 = {
2636
+ name: "RedoIcon",
2637
+ emits: ["click"],
2638
+ props: {
2639
+ title: {
2640
+ type: String
2641
+ },
2642
+ fillColor: {
2643
+ type: String,
2644
+ default: "currentColor"
2645
+ },
2646
+ size: {
2647
+ type: Number,
2648
+ default: 24
2649
+ }
2650
+ }
2651
+ };
2652
+ const _hoisted_1$7 = ["aria-hidden", "aria-label"];
2653
+ const _hoisted_2$6 = ["fill", "width", "height"];
2654
+ const _hoisted_3$6 = { d: "M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z" };
2655
+ const _hoisted_4$6 = { key: 0 };
2656
+ function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) {
2657
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2658
+ "aria-hidden": $props.title ? null : "true",
2659
+ "aria-label": $props.title,
2660
+ class: "material-design-icon redo-icon",
2661
+ role: "img",
2662
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2663
+ }), [
2664
+ (vue.openBlock(), vue.createElementBlock("svg", {
2665
+ fill: $props.fillColor,
2666
+ class: "material-design-icon__svg",
2667
+ width: $props.size,
2668
+ height: $props.size,
2669
+ viewBox: "0 0 24 24"
2670
+ }, [
2671
+ vue.createElementVNode("path", _hoisted_3$6, [
2672
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$6, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2673
+ ])
2674
+ ], 8, _hoisted_2$6))
2675
+ ], 16, _hoisted_1$7);
2676
+ }
2677
+ const Redo = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["render", _sfc_render$4]]);
2678
+ const _sfc_main$7 = {
2679
+ name: "RestoreIcon",
2680
+ emits: ["click"],
2681
+ props: {
2682
+ title: {
2683
+ type: String
2684
+ },
2685
+ fillColor: {
2686
+ type: String,
2687
+ default: "currentColor"
2688
+ },
2689
+ size: {
2690
+ type: Number,
2691
+ default: 24
2692
+ }
2693
+ }
2694
+ };
2695
+ const _hoisted_1$6 = ["aria-hidden", "aria-label"];
2696
+ const _hoisted_2$5 = ["fill", "width", "height"];
2697
+ const _hoisted_3$5 = { d: "M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z" };
2698
+ const _hoisted_4$5 = { key: 0 };
2699
+ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
2700
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2701
+ "aria-hidden": $props.title ? null : "true",
2702
+ "aria-label": $props.title,
2703
+ class: "material-design-icon restore-icon",
2704
+ role: "img",
2705
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2706
+ }), [
2707
+ (vue.openBlock(), vue.createElementBlock("svg", {
2708
+ fill: $props.fillColor,
2709
+ class: "material-design-icon__svg",
2710
+ width: $props.size,
2711
+ height: $props.size,
2712
+ viewBox: "0 0 24 24"
2713
+ }, [
2714
+ vue.createElementVNode("path", _hoisted_3$5, [
2715
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$5, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2716
+ ])
2717
+ ], 8, _hoisted_2$5))
2718
+ ], 16, _hoisted_1$6);
2719
+ }
2720
+ const Restore = /* @__PURE__ */ _export_sfc(_sfc_main$7, [["render", _sfc_render$3]]);
2721
+ const _sfc_main$6 = {
2722
+ name: "UndoIcon",
2723
+ emits: ["click"],
2724
+ props: {
2725
+ title: {
2726
+ type: String
2727
+ },
2728
+ fillColor: {
2729
+ type: String,
2730
+ default: "currentColor"
2731
+ },
2732
+ size: {
2733
+ type: Number,
2734
+ default: 24
2735
+ }
2736
+ }
2737
+ };
2738
+ const _hoisted_1$5 = ["aria-hidden", "aria-label"];
2739
+ const _hoisted_2$4 = ["fill", "width", "height"];
2740
+ const _hoisted_3$4 = { d: "M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z" };
2741
+ const _hoisted_4$4 = { key: 0 };
2742
+ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
2743
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2744
+ "aria-hidden": $props.title ? null : "true",
2745
+ "aria-label": $props.title,
2746
+ class: "material-design-icon undo-icon",
2747
+ role: "img",
2748
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2749
+ }), [
2750
+ (vue.openBlock(), vue.createElementBlock("svg", {
2751
+ fill: $props.fillColor,
2752
+ class: "material-design-icon__svg",
2753
+ width: $props.size,
2754
+ height: $props.size,
2755
+ viewBox: "0 0 24 24"
2756
+ }, [
2757
+ vue.createElementVNode("path", _hoisted_3$4, [
2758
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$4, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
2759
+ ])
2760
+ ], 8, _hoisted_2$4))
2761
+ ], 16, _hoisted_1$5);
2762
+ }
2763
+ const Undo = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["render", _sfc_render$2]]);
2764
+ const _hoisted_1$4 = { class: "editor-topbar" };
2765
+ const _hoisted_2$3 = { class: "editor-topbar__history" };
2766
+ const _hoisted_3$3 = ["aria-label", "title", "disabled"];
2767
+ const _hoisted_4$3 = { class: "editor-topbar__actions" };
2768
+ const _sfc_main$5 = /* @__PURE__ */ vue.defineComponent({
2769
+ __name: "EditorTopBar",
2770
+ props: {
2771
+ loaded: { type: Boolean }
2772
+ },
2773
+ emits: ["save", "cancel"],
2774
+ setup(__props, { emit: __emit }) {
2775
+ const emit = __emit;
2776
+ const context = useEditorContext();
2777
+ const commands = useEditorCommands();
2778
+ const labels = {
2779
+ undo: t("Undo"),
2780
+ redo: t("Redo"),
2781
+ revert: t("Revert all changes"),
2782
+ revertText: t("All edits will be discarded. This cannot be undone by closing the dialog."),
2783
+ zoomIn: t("Zoom in"),
2784
+ zoomOut: t("Zoom out"),
2785
+ resetZoom: t("Reset zoom"),
2786
+ save: t("Save"),
2787
+ cancel: t("Cancel"),
2788
+ history: t("Edit history"),
2789
+ step: t("Edit")
2790
+ };
2791
+ const historySteps = vue.computed(() => context.historyEntries.value.map((entry, index) => ({
2792
+ index,
2793
+ label: entry.label ?? labels.step,
2794
+ active: index === context.historyIndex.value
2795
+ })).reverse());
2796
+ function stepZoom(direction) {
2797
+ const factor = direction === 1 ? 1.5 : 1 / 1.5;
2798
+ context.setViewZoom(context.viewZoom.value * factor);
2799
+ }
2800
+ function resetZoom() {
2801
+ context.setViewZoom(MIN_ZOOM);
2802
+ }
2803
+ async function onRevert() {
2804
+ if (await dialogs.showConfirmation({
2805
+ name: labels.revert,
2806
+ text: labels.revertText,
2807
+ labelConfirm: labels.revert,
2808
+ labelReject: labels.cancel,
2809
+ severity: "warning"
2810
+ })) {
2811
+ commands.revert();
2812
+ }
2813
+ }
2814
+ return (_ctx, _cache) => {
2815
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [
2816
+ _cache[9] || (_cache[9] = vue.createElementVNode("span", { class: "editor-topbar__spacer" }, null, -1)),
2817
+ vue.createElementVNode("div", _hoisted_2$3, [
2818
+ vue.createVNode(vue.unref(NcButton__default.default), {
2819
+ "data-test": "revert",
2820
+ "aria-label": labels.revert,
2821
+ title: labels.revert,
2822
+ disabled: !__props.loaded || !vue.unref(context).canUndo.value,
2823
+ variant: "tertiary",
2824
+ onClick: onRevert
2825
+ }, {
2826
+ icon: vue.withCtx(() => [
2827
+ vue.createVNode(Restore, { size: 20 })
2828
+ ]),
2829
+ _: 1
2830
+ }, 8, ["aria-label", "title", "disabled"]),
2831
+ _cache[7] || (_cache[7] = vue.createElementVNode("span", { class: "editor-topbar__separator" }, null, -1)),
2832
+ vue.createVNode(vue.unref(NcButton__default.default), {
2833
+ "aria-label": labels.undo,
2834
+ title: labels.undo,
2835
+ disabled: !__props.loaded || !vue.unref(context).canUndo.value,
2836
+ variant: "tertiary",
2837
+ onClick: _cache[0] || (_cache[0] = ($event) => vue.unref(context).undo())
2838
+ }, {
2839
+ icon: vue.withCtx(() => [
2840
+ vue.createVNode(Undo, { size: 20 })
2841
+ ]),
2842
+ _: 1
2843
+ }, 8, ["aria-label", "title", "disabled"]),
2844
+ vue.createVNode(vue.unref(NcButton__default.default), {
2845
+ "aria-label": labels.redo,
2846
+ title: labels.redo,
2847
+ disabled: !__props.loaded || !vue.unref(context).canRedo.value,
2848
+ variant: "tertiary",
2849
+ onClick: _cache[1] || (_cache[1] = ($event) => vue.unref(context).redo())
2850
+ }, {
2851
+ icon: vue.withCtx(() => [
2852
+ vue.createVNode(Redo, { size: 20 })
2853
+ ]),
2854
+ _: 1
2855
+ }, 8, ["aria-label", "title", "disabled"]),
2856
+ vue.createVNode(vue.unref(NcActions__default.default), {
2857
+ "aria-label": labels.history,
2858
+ title: labels.history,
2859
+ disabled: !__props.loaded,
2860
+ variant: "tertiary",
2861
+ "data-test": "history"
2862
+ }, {
2863
+ icon: vue.withCtx(() => [
2864
+ vue.createVNode(HistoryIcon, { size: 20 })
2865
+ ]),
2866
+ default: vue.withCtx(() => [
2867
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(historySteps.value, (step) => {
2868
+ return vue.openBlock(), vue.createBlock(vue.unref(NcActionButton__default.default), {
2869
+ key: step.index,
2870
+ "data-test": `history-step-${step.index}`,
2871
+ "aria-current": step.active,
2872
+ onClick: ($event) => vue.unref(context).jumpTo(step.index)
2873
+ }, {
2874
+ icon: vue.withCtx(() => [
2875
+ step.active ? (vue.openBlock(), vue.createBlock(Check, {
2876
+ key: 0,
2877
+ size: 20
2878
+ })) : vue.createCommentVNode("", true)
2879
+ ]),
2880
+ default: vue.withCtx(() => [
2881
+ vue.createTextVNode(" " + vue.toDisplayString(step.label), 1)
2882
+ ]),
2883
+ _: 2
2884
+ }, 1032, ["data-test", "aria-current", "onClick"]);
2885
+ }), 128))
2886
+ ]),
2887
+ _: 1
2888
+ }, 8, ["aria-label", "title", "disabled"]),
2889
+ _cache[8] || (_cache[8] = vue.createElementVNode("span", { class: "editor-topbar__separator" }, null, -1)),
2890
+ vue.createVNode(vue.unref(NcButton__default.default), {
2891
+ "data-test": "zoom-out",
2892
+ "aria-label": labels.zoomOut,
2893
+ title: labels.zoomOut,
2894
+ disabled: !__props.loaded || vue.unref(context).viewZoom.value <= vue.unref(MIN_ZOOM),
2895
+ variant: "tertiary",
2896
+ onClick: _cache[2] || (_cache[2] = ($event) => stepZoom(-1))
2897
+ }, {
2898
+ icon: vue.withCtx(() => [
2899
+ vue.createVNode(MagnifyMinusOutline, { size: 20 })
2900
+ ]),
2901
+ _: 1
2902
+ }, 8, ["aria-label", "title", "disabled"]),
2903
+ vue.createElementVNode("button", {
2904
+ type: "button",
2905
+ class: "editor-topbar__zoom",
2906
+ "data-test": "zoom-reset",
2907
+ "aria-label": labels.resetZoom,
2908
+ title: labels.resetZoom,
2909
+ disabled: !__props.loaded,
2910
+ onClick: resetZoom
2911
+ }, vue.toDisplayString(Math.round(vue.unref(context).viewZoom.value * 100)) + "% ", 9, _hoisted_3$3),
2912
+ vue.createVNode(vue.unref(NcButton__default.default), {
2913
+ "data-test": "zoom-in",
2914
+ "aria-label": labels.zoomIn,
2915
+ title: labels.zoomIn,
2916
+ disabled: !__props.loaded || vue.unref(context).viewZoom.value >= vue.unref(MAX_ZOOM),
2917
+ variant: "tertiary",
2918
+ onClick: _cache[3] || (_cache[3] = ($event) => stepZoom(1))
2919
+ }, {
2920
+ icon: vue.withCtx(() => [
2921
+ vue.createVNode(MagnifyPlusOutline, { size: 20 })
2922
+ ]),
2923
+ _: 1
2924
+ }, 8, ["aria-label", "title", "disabled"])
2925
+ ]),
2926
+ vue.createElementVNode("div", _hoisted_4$3, [
2927
+ vue.createVNode(vue.unref(NcButton__default.default), {
2928
+ "data-test": "cancel",
2929
+ class: "editor-topbar__cancel-text",
2930
+ variant: "tertiary",
2931
+ onClick: _cache[4] || (_cache[4] = ($event) => emit("cancel"))
2932
+ }, {
2933
+ default: vue.withCtx(() => [
2934
+ vue.createTextVNode(vue.toDisplayString(labels.cancel), 1)
2935
+ ]),
2936
+ _: 1
2937
+ }),
2938
+ vue.createVNode(vue.unref(NcButton__default.default), {
2939
+ "data-test": "cancel-icon",
2940
+ class: "editor-topbar__cancel-icon",
2941
+ "aria-label": labels.cancel,
2942
+ title: labels.cancel,
2943
+ variant: "tertiary",
2944
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("cancel"))
2945
+ }, {
2946
+ icon: vue.withCtx(() => [
2947
+ vue.createVNode(Close, { size: 20 })
2948
+ ]),
2949
+ _: 1
2950
+ }, 8, ["aria-label", "title"]),
2951
+ vue.createVNode(vue.unref(NcButton__default.default), {
2952
+ "data-test": "save",
2953
+ variant: "primary",
2954
+ disabled: !__props.loaded,
2955
+ onClick: _cache[6] || (_cache[6] = ($event) => emit("save"))
2956
+ }, {
2957
+ default: vue.withCtx(() => [
2958
+ vue.createTextVNode(vue.toDisplayString(labels.save), 1)
2959
+ ]),
2960
+ _: 1
2961
+ }, 8, ["disabled"])
2962
+ ])
2963
+ ]);
2964
+ };
2965
+ }
2966
+ });
2967
+ const EditorTopBar = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-9ba9bd23"]]);
2968
+ const _sfc_main$4 = {
2969
+ name: "ContentCopyIcon",
2970
+ emits: ["click"],
2971
+ props: {
2972
+ title: {
2973
+ type: String
2974
+ },
2975
+ fillColor: {
2976
+ type: String,
2977
+ default: "currentColor"
2978
+ },
2979
+ size: {
2980
+ type: Number,
2981
+ default: 24
2982
+ }
2983
+ }
2984
+ };
2985
+ const _hoisted_1$3 = ["aria-hidden", "aria-label"];
2986
+ const _hoisted_2$2 = ["fill", "width", "height"];
2987
+ const _hoisted_3$2 = { d: "M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" };
2988
+ const _hoisted_4$2 = { key: 0 };
2989
+ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
2990
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
2991
+ "aria-hidden": $props.title ? null : "true",
2992
+ "aria-label": $props.title,
2993
+ class: "material-design-icon content-copy-icon",
2994
+ role: "img",
2995
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2996
+ }), [
2997
+ (vue.openBlock(), vue.createElementBlock("svg", {
2998
+ fill: $props.fillColor,
2999
+ class: "material-design-icon__svg",
3000
+ width: $props.size,
3001
+ height: $props.size,
3002
+ viewBox: "0 0 24 24"
3003
+ }, [
3004
+ vue.createElementVNode("path", _hoisted_3$2, [
3005
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$2, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
3006
+ ])
3007
+ ], 8, _hoisted_2$2))
3008
+ ], 16, _hoisted_1$3);
3009
+ }
3010
+ const ContentCopy = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["render", _sfc_render$1]]);
3011
+ const _sfc_main$3 = {
3012
+ name: "DeleteIcon",
3013
+ emits: ["click"],
3014
+ props: {
3015
+ title: {
3016
+ type: String
3017
+ },
3018
+ fillColor: {
3019
+ type: String,
3020
+ default: "currentColor"
3021
+ },
3022
+ size: {
3023
+ type: Number,
3024
+ default: 24
3025
+ }
3026
+ }
3027
+ };
3028
+ const _hoisted_1$2 = ["aria-hidden", "aria-label"];
3029
+ const _hoisted_2$1 = ["fill", "width", "height"];
3030
+ const _hoisted_3$1 = { d: "M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" };
3031
+ const _hoisted_4$1 = { key: 0 };
3032
+ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
3033
+ return vue.openBlock(), vue.createElementBlock("span", vue.mergeProps(_ctx.$attrs, {
3034
+ "aria-hidden": $props.title ? null : "true",
3035
+ "aria-label": $props.title,
3036
+ class: "material-design-icon delete-icon",
3037
+ role: "img",
3038
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
3039
+ }), [
3040
+ (vue.openBlock(), vue.createElementBlock("svg", {
3041
+ fill: $props.fillColor,
3042
+ class: "material-design-icon__svg",
3043
+ width: $props.size,
3044
+ height: $props.size,
3045
+ viewBox: "0 0 24 24"
3046
+ }, [
3047
+ vue.createElementVNode("path", _hoisted_3$1, [
3048
+ $props.title ? (vue.openBlock(), vue.createElementBlock("title", _hoisted_4$1, vue.toDisplayString($props.title), 1)) : vue.createCommentVNode("", true)
3049
+ ])
3050
+ ], 8, _hoisted_2$1))
3051
+ ], 16, _hoisted_1$2);
3052
+ }
3053
+ const Delete = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render]]);
3054
+ const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
3055
+ __name: "SelectionToolbar",
3056
+ props: {
3057
+ box: {}
3058
+ },
3059
+ emits: ["duplicate", "delete"],
3060
+ setup(__props, { emit: __emit }) {
3061
+ const emit = __emit;
3062
+ const duplicateLabel = t("Duplicate");
3063
+ const deleteLabel = t("Delete");
3064
+ return (_ctx, _cache) => {
3065
+ return vue.openBlock(), vue.createBlock(GlassSurface, {
3066
+ variant: "pill",
3067
+ class: "selection-toolbar",
3068
+ "data-test": "selection-toolbar",
3069
+ style: vue.normalizeStyle({
3070
+ insetInlineStart: `${__props.box.x + __props.box.width / 2}px`,
3071
+ // Above the selection as designed, clearing the rotate handle;
3072
+ // below only when the top edge would clip it
3073
+ insetBlockStart: __props.box.y - 76 >= 4 ? `${__props.box.y - 76}px` : `${__props.box.y + __props.box.height + 12}px`
3074
+ })
3075
+ }, {
3076
+ default: vue.withCtx(() => [
3077
+ vue.createVNode(vue.unref(NcButton__default.default), {
3078
+ "data-test": "duplicate",
3079
+ "aria-label": vue.unref(duplicateLabel),
3080
+ title: vue.unref(duplicateLabel),
3081
+ variant: "tertiary",
3082
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("duplicate"))
3083
+ }, {
3084
+ icon: vue.withCtx(() => [
3085
+ vue.createVNode(ContentCopy, { size: 18 })
3086
+ ]),
3087
+ _: 1
3088
+ }, 8, ["aria-label", "title"]),
3089
+ vue.createVNode(vue.unref(NcButton__default.default), {
3090
+ "data-test": "delete",
3091
+ "aria-label": vue.unref(deleteLabel),
3092
+ title: vue.unref(deleteLabel),
3093
+ variant: "tertiary",
3094
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("delete"))
3095
+ }, {
3096
+ icon: vue.withCtx(() => [
3097
+ vue.createVNode(Delete, { size: 18 })
3098
+ ]),
3099
+ _: 1
3100
+ }, 8, ["aria-label", "title"])
3101
+ ]),
3102
+ _: 1
3103
+ }, 8, ["style"]);
3104
+ };
3105
+ }
3106
+ });
3107
+ const SelectionToolbar = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-09c1d16e"]]);
3108
+ const _hoisted_1$1 = ["aria-label"];
3109
+ const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
3110
+ __name: "TextOverlay",
3111
+ props: {
3112
+ x: {},
3113
+ y: {},
3114
+ fontSize: {},
3115
+ color: {},
3116
+ initial: {}
3117
+ },
3118
+ emits: ["confirm", "cancel"],
3119
+ setup(__props, { emit: __emit }) {
3120
+ const props = __props;
3121
+ const emit = __emit;
3122
+ const inputLabel = t("Annotation text");
3123
+ const value = vue.ref(props.initial);
3124
+ const input = vue.useTemplateRef("input");
3125
+ function autosize() {
3126
+ const element = input.value;
3127
+ if (element === null) {
3128
+ return;
3129
+ }
3130
+ element.style.width = "0";
3131
+ element.style.height = "0";
3132
+ element.style.width = `${Math.max(element.scrollWidth + 4, props.fontSize * 2)}px`;
3133
+ element.style.height = `${Math.max(element.scrollHeight, props.fontSize * 1.2)}px`;
3134
+ }
3135
+ vue.onMounted(() => {
3136
+ autosize();
3137
+ input.value.focus();
3138
+ input.value.select();
3139
+ });
3140
+ function onEnter(event) {
3141
+ if (!event.shiftKey) {
3142
+ event.preventDefault();
3143
+ emit("confirm", value.value);
3144
+ }
3145
+ }
3146
+ return (_ctx, _cache) => {
3147
+ return vue.withDirectives((vue.openBlock(), vue.createElementBlock("textarea", {
3148
+ ref_key: "input",
3149
+ ref: input,
3150
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => value.value = $event),
3151
+ class: "text-overlay",
3152
+ "aria-label": vue.unref(inputLabel),
3153
+ "data-test": "text-overlay",
3154
+ style: vue.normalizeStyle({
3155
+ insetInlineStart: `${__props.x}px`,
3156
+ insetBlockStart: `${__props.y}px`,
3157
+ fontSize: `${__props.fontSize}px`,
3158
+ color: __props.color
3159
+ }),
3160
+ rows: "1",
3161
+ onInput: autosize,
3162
+ onKeydown: [
3163
+ vue.withKeys(onEnter, ["enter"]),
3164
+ _cache[1] || (_cache[1] = vue.withKeys(vue.withModifiers(($event) => emit("cancel"), ["stop"]), ["esc"]))
3165
+ ],
3166
+ onBlur: _cache[2] || (_cache[2] = ($event) => emit("confirm", value.value))
3167
+ }, null, 44, _hoisted_1$1)), [
3168
+ [vue.vModelText, value.value]
3169
+ ]);
3170
+ };
3171
+ }
3172
+ });
3173
+ const TextOverlay = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-bbba034b"]]);
3174
+ function primaryColor() {
3175
+ const value = getComputedStyle(document.body).getPropertyValue("--color-primary-element").trim();
3176
+ return value !== "" ? value : "#0082c9";
3177
+ }
3178
+ function ambientColor(canvas) {
3179
+ const sample = document.createElement("canvas");
3180
+ sample.width = 8;
3181
+ sample.height = 8;
3182
+ const context = sample.getContext("2d");
3183
+ if (context === null) {
3184
+ return "88, 86, 112";
3185
+ }
3186
+ context.drawImage(canvas, 0, 0, 8, 8);
3187
+ let data;
3188
+ try {
3189
+ data = context.getImageData(0, 0, 8, 8).data;
3190
+ } catch {
3191
+ return "88, 86, 112";
3192
+ }
3193
+ let r = 0;
3194
+ let g = 0;
3195
+ let b = 0;
3196
+ let total = 0;
3197
+ for (let i = 0; i < data.length; i += 4) {
3198
+ const saturation = Math.max(data[i], data[i + 1], data[i + 2]) - Math.min(data[i], data[i + 1], data[i + 2]);
3199
+ const weight = saturation + 8;
3200
+ r += data[i] * weight;
3201
+ g += data[i + 1] * weight;
3202
+ b += data[i + 2] * weight;
3203
+ total += weight;
3204
+ }
3205
+ return `${Math.round(r / total)}, ${Math.round(g / total)}, ${Math.round(b / total)}`;
3206
+ }
3207
+ function ambientBackdrop(canvas) {
3208
+ const sample = document.createElement("canvas");
3209
+ const width = canvas instanceof HTMLImageElement ? canvas.naturalWidth : canvas.width;
3210
+ const height = canvas instanceof HTMLImageElement ? canvas.naturalHeight : canvas.height;
3211
+ if (width === 0 || height === 0) {
3212
+ return "";
3213
+ }
3214
+ sample.width = 24;
3215
+ sample.height = Math.max(1, Math.round(24 * height / width));
3216
+ const context = sample.getContext("2d");
3217
+ if (context === null) {
3218
+ return "";
3219
+ }
3220
+ context.drawImage(canvas, 0, 0, sample.width, sample.height);
3221
+ try {
3222
+ return sample.toDataURL();
3223
+ } catch {
3224
+ return "";
3225
+ }
3226
+ }
3227
+ function useAmbient(source) {
3228
+ const ambient = vue.ref("88, 86, 112");
3229
+ const backdrop = vue.ref("");
3230
+ vue.watch(source, (image) => {
3231
+ if (image !== null) {
3232
+ ambient.value = ambientColor(image);
3233
+ backdrop.value = ambientBackdrop(image);
3234
+ }
3235
+ });
3236
+ return { ambient, backdrop };
3237
+ }
3238
+ function useAnnouncements(context) {
3239
+ const message = vue.ref("");
3240
+ const modeLabels = {
3241
+ select: t("Select"),
3242
+ crop: t("Crop"),
3243
+ finetune: t("Adjust"),
3244
+ filter: t("Filter"),
3245
+ annotate: t("Annotate"),
3246
+ sticker: t("Sticker"),
3247
+ redact: t("Redact")
3248
+ };
3249
+ vue.watch(context.activeMode, (mode) => {
3250
+ message.value = t("{mode} mode", { mode: modeLabels[mode] ?? mode });
3251
+ });
3252
+ let annotationCount = 0;
3253
+ vue.watch(context.state, (state) => {
3254
+ if (state.annotations.length > annotationCount) {
3255
+ message.value = t("Annotation added");
3256
+ } else if (state.annotations.length < annotationCount) {
3257
+ message.value = t("Annotation removed");
3258
+ }
3259
+ annotationCount = state.annotations.length;
3260
+ });
3261
+ vue.watch(() => context.state.value.preset, (preset) => {
3262
+ message.value = preset === "none" ? t("Filter removed") : t("Filter applied");
3263
+ });
3264
+ return message;
3265
+ }
3266
+ const TEXT_ENTRY = "input, textarea, select, [contenteditable]";
3267
+ const ACTIVATABLE = 'button, [role="button"], a[href], summary';
3268
+ function ownsTextEntry(target) {
3269
+ return target instanceof HTMLElement && target.closest(TEXT_ENTRY) !== null;
3270
+ }
3271
+ function ownsSpaceKey(target) {
3272
+ return target instanceof HTMLElement && target.closest(`${TEXT_ENTRY}, ${ACTIVATABLE}`) !== null;
3273
+ }
3274
+ const NUDGE_KEYS = {
3275
+ ArrowLeft: [-1, 0],
3276
+ ArrowRight: [1, 0],
3277
+ ArrowUp: [0, -1],
3278
+ ArrowDown: [0, 1]
3279
+ };
3280
+ function useEditorShortcuts(deps) {
3281
+ const { context } = deps;
3282
+ let nudging = false;
3283
+ function nudgeSelection(event) {
3284
+ const id = context.selectedId.value;
3285
+ const direction = NUDGE_KEYS[event.key];
3286
+ if (id === null || direction === void 0) {
3287
+ return;
3288
+ }
3289
+ event.preventDefault();
3290
+ const step = event.shiftKey ? 10 : 1;
3291
+ const state = context.state.value;
3292
+ nudging = true;
3293
+ context.preview({
3294
+ ...state,
3295
+ annotations: state.annotations.map((annotation) => annotation.id === id ? translateAnnotation(annotation, direction[0] * step, direction[1] * step) : annotation)
3296
+ });
3297
+ }
3298
+ function onKeydown(event) {
3299
+ if (deps.isTextEditing() || ownsTextEntry(event.target)) {
3300
+ return;
3301
+ }
3302
+ const meta = event.ctrlKey || event.metaKey;
3303
+ if (meta && !event.shiftKey && event.key.toLowerCase() === "z") {
3304
+ event.preventDefault();
3305
+ context.undo();
3306
+ return;
3307
+ }
3308
+ if (meta && event.shiftKey && event.key.toLowerCase() === "z" || meta && event.key.toLowerCase() === "y") {
3309
+ event.preventDefault();
3310
+ context.redo();
3311
+ return;
3312
+ }
3313
+ if (event.key === "+" || event.key === "=") {
3314
+ context.setViewZoom(context.viewZoom.value * 1.25);
3315
+ return;
3316
+ }
3317
+ if (event.key === "-") {
3318
+ context.setViewZoom(context.viewZoom.value / 1.25);
3319
+ return;
3320
+ }
3321
+ if (event.key in NUDGE_KEYS && context.selectedId.value !== null) {
3322
+ nudgeSelection(event);
3323
+ return;
3324
+ }
3325
+ if ((event.key === "Delete" || event.key === "Backspace") && context.selectedId.value !== null) {
3326
+ event.preventDefault();
3327
+ deps.onDelete();
3328
+ } else if (event.key === "Escape") {
3329
+ deps.onEscape();
3330
+ }
3331
+ }
3332
+ function onKeyup(event) {
3333
+ if (ownsTextEntry(event.target)) {
3334
+ return;
3335
+ }
3336
+ if (nudging && event.key in NUDGE_KEYS) {
3337
+ nudging = false;
3338
+ context.commit(context.state.value, t("Move or resize"));
3339
+ }
3340
+ }
3341
+ vue.onMounted(() => {
3342
+ window.addEventListener("keydown", onKeydown);
3343
+ window.addEventListener("keyup", onKeyup);
3344
+ });
3345
+ vue.onBeforeUnmount(() => {
3346
+ window.removeEventListener("keydown", onKeydown);
3347
+ window.removeEventListener("keyup", onKeyup);
3348
+ });
3349
+ }
3350
+ function needsCors(url) {
3351
+ let target;
3352
+ try {
3353
+ target = new URL(url, window.location.href);
3354
+ } catch {
3355
+ return false;
3356
+ }
3357
+ return (target.protocol === "http:" || target.protocol === "https:") && target.origin !== window.location.origin;
3358
+ }
3359
+ async function loadImage(source) {
3360
+ const url = typeof source === "string" ? source : URL.createObjectURL(source);
3361
+ try {
3362
+ return await new Promise((resolve, reject) => {
3363
+ const image = new Image();
3364
+ if (needsCors(url)) {
3365
+ image.crossOrigin = "anonymous";
3366
+ }
3367
+ image.onload = () => resolve(image);
3368
+ image.onerror = () => reject(new Error(t("Image could not be decoded")));
3369
+ image.src = url;
3370
+ });
3371
+ } finally {
3372
+ if (typeof source !== "string") {
3373
+ URL.revokeObjectURL(url);
3374
+ }
3375
+ }
3376
+ }
3377
+ async function canvasToBlob(canvas, type = "image/png", quality) {
3378
+ return new Promise((resolve, reject) => {
3379
+ canvas.toBlob((blob) => {
3380
+ if (blob === null) {
3381
+ reject(new Error(t("Canvas could not be encoded")));
3382
+ return;
3383
+ }
3384
+ resolve(blob);
3385
+ }, type, quality);
3386
+ });
3387
+ }
3388
+ function useExportImage(deps) {
3389
+ async function exportImage(options = {}) {
3390
+ const oriented = deps.oriented();
3391
+ if (oriented === null) {
3392
+ throw new Error("No image loaded");
3393
+ }
3394
+ const source = deps.source();
3395
+ if (source !== null && source.type !== "" && isPristine(deps.getState()) && options.maxSize === void 0 && (options.format === void 0 || options.format === source.type)) {
3396
+ return { blob: source, width: oriented.width, height: oriented.height, mimeType: source.type };
3397
+ }
3398
+ const canvas = renderToCanvas(oriented, deps.getState(), options.maxSize);
3399
+ const mimeType = options.format ?? "image/png";
3400
+ try {
3401
+ const blob = await canvasToBlob(canvas, mimeType, options.quality);
3402
+ return { blob, width: canvas.width, height: canvas.height, mimeType };
3403
+ } catch (error) {
3404
+ if (error instanceof DOMException && error.name === "SecurityError") {
3405
+ throw new Error(
3406
+ t("The image cannot be exported because it was loaded without cross-origin access"),
3407
+ { cause: error }
3408
+ );
3409
+ }
3410
+ throw error;
3411
+ }
3412
+ }
3413
+ async function save() {
3414
+ try {
3415
+ deps.onSaved(await exportImage(deps.saveOptions()));
3416
+ } catch (error) {
3417
+ deps.onError(error instanceof Error ? error : new Error(String(error)));
3418
+ }
3419
+ }
3420
+ return { exportImage, save };
3421
+ }
3422
+ function useTextEditing(deps) {
3423
+ const { context } = deps;
3424
+ const textEdit = vue.ref(null);
3425
+ function startTextEdit(position, existing) {
3426
+ const options = deps.viewOptions();
3427
+ const oriented = deps.oriented();
3428
+ if (options === null || oriented === null) {
3429
+ return;
3430
+ }
3431
+ const origin = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
3432
+ textEdit.value = {
3433
+ sceneX: position.x,
3434
+ sceneY: position.y,
3435
+ screenX: options.offset.x + (position.x - origin.x) * options.scale,
3436
+ screenY: options.offset.y + (position.y - origin.y) * options.scale,
3437
+ screenFontSize: (existing?.fontSize ?? context.fontSize.value) * options.scale,
3438
+ color: existing?.color ?? context.drawColor.value,
3439
+ value: existing?.text ?? "",
3440
+ id: existing?.id ?? null
3441
+ };
3442
+ }
3443
+ function confirmTextEdit(text) {
3444
+ const edit = textEdit.value;
3445
+ textEdit.value = null;
3446
+ if (edit === null) {
3447
+ return;
3448
+ }
3449
+ const state = context.state.value;
3450
+ const trimmed = text.trim();
3451
+ if (edit.id !== null) {
3452
+ context.commit({
3453
+ ...state,
3454
+ annotations: trimmed === "" ? state.annotations.filter((annotation) => annotation.id !== edit.id) : state.annotations.map((annotation) => annotation.id === edit.id ? { ...annotation, text: trimmed } : annotation)
3455
+ }, trimmed === "" ? t("Delete") : t("Text"));
3456
+ } else if (trimmed !== "") {
3457
+ context.commit({
3458
+ ...state,
3459
+ annotations: [...state.annotations, {
3460
+ id: newId(),
3461
+ type: "text",
3462
+ x: edit.sceneX,
3463
+ y: edit.sceneY,
3464
+ text: trimmed,
3465
+ color: edit.color,
3466
+ fontSize: context.fontSize.value,
3467
+ rotation: 0
3468
+ }]
3469
+ }, t("Text"));
3470
+ }
3471
+ }
3472
+ return { textEdit, startTextEdit, confirmTextEdit };
3473
+ }
3474
+ function useWheelControls(element, context) {
3475
+ let dragFrom = null;
3476
+ let pinch = null;
3477
+ const pointers = /* @__PURE__ */ new Map();
3478
+ const spaceHeld = vue.shallowRef(false);
3479
+ const toolFreeDrag = () => context.activeTool.value === "adjust";
3480
+ const zoomed = () => context.viewZoom.value > MIN_ZOOM;
3481
+ const panArmed = vue.computed(() => context.viewZoom.value > MIN_ZOOM && (spaceHeld.value || context.activeTool.value === "adjust"));
3482
+ function anchorAt(position) {
3483
+ const rect = element.value?.getBoundingClientRect();
3484
+ if (rect === void 0) {
3485
+ return void 0;
3486
+ }
3487
+ return {
3488
+ x: position.x - rect.x - rect.width / 2,
3489
+ y: position.y - rect.y - rect.height / 2
3490
+ };
3491
+ }
3492
+ function currentPinch() {
3493
+ if (pointers.size !== 2) {
3494
+ return null;
3495
+ }
3496
+ const [a, b] = [...pointers.values()];
3497
+ return {
3498
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
3499
+ centroid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
3500
+ };
3501
+ }
3502
+ function onWheel(event) {
3503
+ event.preventDefault();
3504
+ if (event.deltaY === 0) {
3505
+ return;
3506
+ }
3507
+ const height = element.value?.clientHeight ?? window.innerHeight;
3508
+ const factor = wheelZoomFactor(event.deltaY, event.deltaMode, height);
3509
+ context.setViewZoom(context.viewZoom.value * factor, anchorAt({ x: event.clientX, y: event.clientY }));
3510
+ }
3511
+ function onPointerDown(event) {
3512
+ if (event.pointerType === "touch") {
3513
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
3514
+ if (pointers.size === 2) {
3515
+ pinch = currentPinch();
3516
+ context.panning.value = true;
3517
+ event.stopPropagation();
3518
+ }
3519
+ return;
3520
+ }
3521
+ const wantsPan = event.button === 1 || event.button === 0 && (spaceHeld.value || toolFreeDrag());
3522
+ if (!wantsPan || !zoomed()) {
3523
+ return;
3524
+ }
3525
+ event.preventDefault();
3526
+ event.stopPropagation();
3527
+ dragFrom = {
3528
+ pointer: { x: event.clientX, y: event.clientY },
3529
+ pan: context.viewPan.value
3530
+ };
3531
+ context.panning.value = true;
3532
+ element.value?.setPointerCapture(event.pointerId);
3533
+ }
3534
+ function onPointerMove(event) {
3535
+ if (event.pointerType === "touch" && pointers.has(event.pointerId)) {
3536
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
3537
+ const next = currentPinch();
3538
+ if (next === null || pinch === null) {
3539
+ return;
3540
+ }
3541
+ event.stopPropagation();
3542
+ const ratio = next.distance / pinch.distance;
3543
+ if (Math.abs(ratio - 1) > PINCH_TOLERANCE) {
3544
+ context.setViewZoom(context.viewZoom.value * ratio, anchorAt(next.centroid));
3545
+ } else {
3546
+ context.setViewPan({
3547
+ x: context.viewPan.value.x + next.centroid.x - pinch.centroid.x,
3548
+ y: context.viewPan.value.y + next.centroid.y - pinch.centroid.y
3549
+ });
3550
+ }
3551
+ pinch = next;
3552
+ return;
3553
+ }
3554
+ if (dragFrom === null) {
3555
+ return;
3556
+ }
3557
+ event.stopPropagation();
3558
+ context.setViewPan({
3559
+ x: dragFrom.pan.x + (event.clientX - dragFrom.pointer.x),
3560
+ y: dragFrom.pan.y + (event.clientY - dragFrom.pointer.y)
3561
+ });
3562
+ }
3563
+ function onPointerUp(event) {
3564
+ if (event !== void 0) {
3565
+ pointers.delete(event.pointerId);
3566
+ }
3567
+ pinch = currentPinch();
3568
+ dragFrom = null;
3569
+ if (pointers.size < 2) {
3570
+ context.panning.value = false;
3571
+ }
3572
+ }
3573
+ function onKeydown(event) {
3574
+ if (event.code !== "Space" || ownsSpaceKey(event.target)) {
3575
+ return;
3576
+ }
3577
+ event.preventDefault();
3578
+ spaceHeld.value = true;
3579
+ }
3580
+ function onKeyup(event) {
3581
+ if (event.code === "Space") {
3582
+ spaceHeld.value = false;
3583
+ }
3584
+ }
3585
+ vue.onMounted(() => {
3586
+ const target = element.value;
3587
+ target?.addEventListener("wheel", onWheel, { passive: false });
3588
+ target?.addEventListener("pointerdown", onPointerDown, { capture: true });
3589
+ target?.addEventListener("pointermove", onPointerMove, { capture: true });
3590
+ target?.addEventListener("pointerup", onPointerUp);
3591
+ target?.addEventListener("pointercancel", onPointerUp);
3592
+ window.addEventListener("keydown", onKeydown);
3593
+ window.addEventListener("keyup", onKeyup);
3594
+ });
3595
+ vue.onBeforeUnmount(() => {
3596
+ const target = element.value;
3597
+ target?.removeEventListener("wheel", onWheel);
3598
+ target?.removeEventListener("pointerdown", onPointerDown, { capture: true });
3599
+ target?.removeEventListener("pointermove", onPointerMove, { capture: true });
3600
+ target?.removeEventListener("pointerup", onPointerUp);
3601
+ target?.removeEventListener("pointercancel", onPointerUp);
3602
+ window.removeEventListener("keydown", onKeydown);
3603
+ window.removeEventListener("keyup", onKeyup);
3604
+ });
3605
+ return { panArmed };
3606
+ }
3607
+ const DURATION = 0.3;
3608
+ function prefersReducedMotion() {
3609
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3610
+ }
3611
+ function pivotOnCenter(target) {
3612
+ target.node.offset(target.pivot);
3613
+ target.node.position(target.pivot);
3614
+ }
3615
+ function playTransition(kind, deps, context) {
3616
+ if (prefersReducedMotion()) {
3617
+ return;
3618
+ }
3619
+ const { scale } = deps;
3620
+ const easing = Konva__default.default.Easings.EaseInOut;
3621
+ const ratio = context.previousScale / scale;
3622
+ for (const target of deps.targets) {
3623
+ const { node } = target;
3624
+ node.setAttrs({ x: 0, y: 0, offsetX: 0, offsetY: 0, rotation: 0, scaleX: 1, scaleY: 1, opacity: 1 });
3625
+ switch (kind) {
3626
+ case "load":
3627
+ pivotOnCenter(target);
3628
+ node.opacity(0);
3629
+ node.scale({ x: 0.96, y: 0.96 });
3630
+ node.to({ opacity: 1, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3631
+ break;
3632
+ case "rotate-cw":
3633
+ case "rotate-ccw":
3634
+ pivotOnCenter(target);
3635
+ node.rotation(kind === "rotate-cw" ? -90 : 90);
3636
+ node.scale({ x: ratio, y: ratio });
3637
+ node.to({ rotation: 0, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3638
+ break;
3639
+ case "flip-h":
3640
+ pivotOnCenter(target);
3641
+ node.scaleX(-1);
3642
+ node.to({ scaleX: 1, duration: DURATION, easing });
3643
+ break;
3644
+ case "flip-v":
3645
+ pivotOnCenter(target);
3646
+ node.scaleY(-1);
3647
+ node.to({ scaleY: 1, duration: DURATION, easing });
3648
+ break;
3649
+ case "crop": {
3650
+ const { previousScale, previousOffset, previousOrigin } = context;
3651
+ node.scale({ x: ratio, y: ratio });
3652
+ node.position({
3653
+ x: (previousOffset.x - previousOrigin.x * previousScale - (deps.offset.x - deps.origin.x * scale)) * target.unit,
3654
+ y: (previousOffset.y - previousOrigin.y * previousScale - (deps.offset.y - deps.origin.y * scale)) * target.unit
3655
+ });
3656
+ node.to({ x: 0, y: 0, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3657
+ break;
3658
+ }
3659
+ }
3660
+ }
3661
+ }
3662
+ function clampCropBox(bounds, oldBox, newBox, keepRatio) {
3663
+ const right = bounds.x + bounds.width;
3664
+ const bottom = bounds.y + bounds.height;
3665
+ let { x, y, width, height } = newBox;
3666
+ if (x < bounds.x) {
3667
+ width -= bounds.x - x;
3668
+ x = bounds.x;
3669
+ }
3670
+ if (y < bounds.y) {
3671
+ height -= bounds.y - y;
3672
+ y = bounds.y;
3673
+ }
3674
+ width = Math.min(width, right - x);
3675
+ height = Math.min(height, bottom - y);
3676
+ if (keepRatio && oldBox.height > 0) {
3677
+ const ratio = oldBox.width / oldBox.height;
3678
+ if (width / height > ratio) {
3679
+ width = height * ratio;
3680
+ } else {
3681
+ height = width / ratio;
3682
+ }
3683
+ if (Math.abs(newBox.x - oldBox.x) > 0.01) {
3684
+ x = Math.min(newBox.x + newBox.width, right) - width;
3685
+ }
3686
+ if (Math.abs(newBox.y - oldBox.y) > 0.01) {
3687
+ y = Math.min(newBox.y + newBox.height, bottom) - height;
3688
+ }
3689
+ }
3690
+ return {
3691
+ ...newBox,
3692
+ x,
3693
+ y,
3694
+ width: Math.max(8, width),
3695
+ height: Math.max(8, height)
3696
+ };
3697
+ }
3698
+ function attachCropOverlay(deps) {
3699
+ const view = { oriented: deps.oriented, scale: deps.scale, offset: deps.offset };
3700
+ const toStage = (rect) => ({
3701
+ x: view.offset.x + rect.x * view.scale,
3702
+ y: view.offset.y + rect.y * view.scale,
3703
+ width: rect.width * view.scale,
3704
+ height: rect.height * view.scale
3705
+ });
3706
+ const toScene = (rect) => ({
3707
+ x: (rect.x - view.offset.x) / view.scale,
3708
+ y: (rect.y - view.offset.y) / view.scale,
3709
+ width: rect.width / view.scale,
3710
+ height: rect.height / view.scale
3711
+ });
3712
+ let imageBounds = toStage({ x: 0, y: 0, ...view.oriented });
3713
+ const layer = new Konva__default.default.Layer({ name: "crop" });
3714
+ const makeShade = () => new Konva__default.default.Rect({
3715
+ fill: "rgba(0, 0, 0, 0.5)",
3716
+ listening: false
3717
+ });
3718
+ const shadeTop = makeShade();
3719
+ const shadeLeft = makeShade();
3720
+ const shadeRight = makeShade();
3721
+ const shadeBottom = makeShade();
3722
+ for (const shade of [shadeTop, shadeLeft, shadeRight, shadeBottom]) {
3723
+ layer.add(shade);
3724
+ }
3725
+ const cropNode = new Konva__default.default.Rect({
3726
+ // Named so it can be told apart from the four shade rects
3727
+ name: "crop-rect",
3728
+ ...toStage(clampRect(deps.initial ?? { x: 0, y: 0, ...view.oriented }, view.oriented)),
3729
+ stroke: "rgba(255, 255, 255, 0.9)",
3730
+ strokeWidth: 1,
3731
+ draggable: true,
3732
+ strokeScaleEnabled: false
3733
+ });
3734
+ layer.add(cropNode);
3735
+ const stageRect = () => ({
3736
+ x: cropNode.x(),
3737
+ y: cropNode.y(),
3738
+ width: cropNode.width() * cropNode.scaleX(),
3739
+ height: cropNode.height() * cropNode.scaleY()
3740
+ });
3741
+ const gridLines = Array.from({ length: 4 }, () => new Konva__default.default.Line({
3742
+ stroke: "rgba(255, 255, 255, 0.35)",
3743
+ strokeWidth: 1,
3744
+ listening: false
3745
+ }));
3746
+ for (const line of gridLines) {
3747
+ layer.add(line);
3748
+ }
3749
+ const clampToImage = (rect) => {
3750
+ const x = Math.min(Math.max(rect.x, imageBounds.x), imageBounds.x + imageBounds.width - rect.width);
3751
+ const y = Math.min(Math.max(rect.y, imageBounds.y), imageBounds.y + imageBounds.height - rect.height);
3752
+ return { ...rect, x, y };
3753
+ };
3754
+ const updateOverlay = () => {
3755
+ const rect = stageRect();
3756
+ const { x, y, width, height } = imageBounds;
3757
+ shadeTop.setAttrs({ x, y, width, height: rect.y - y });
3758
+ shadeLeft.setAttrs({ x, y: rect.y, width: rect.x - x, height: rect.height });
3759
+ shadeRight.setAttrs({ x: rect.x + rect.width, y: rect.y, width: x + width - rect.x - rect.width, height: rect.height });
3760
+ shadeBottom.setAttrs({ x, y: rect.y + rect.height, width, height: y + height - rect.y - rect.height });
3761
+ gridLines[0].points([rect.x + rect.width / 3, rect.y, rect.x + rect.width / 3, rect.y + rect.height]);
3762
+ gridLines[1].points([rect.x + rect.width * 2 / 3, rect.y, rect.x + rect.width * 2 / 3, rect.y + rect.height]);
3763
+ gridLines[2].points([rect.x, rect.y + rect.height / 3, rect.x + rect.width, rect.y + rect.height / 3]);
3764
+ gridLines[3].points([rect.x, rect.y + rect.height * 2 / 3, rect.x + rect.width, rect.y + rect.height * 2 / 3]);
3765
+ };
3766
+ let ratioLocked = false;
3767
+ const transformer = new Konva__default.default.Transformer({
3768
+ nodes: [cropNode],
3769
+ rotateEnabled: false,
3770
+ flipEnabled: false,
3771
+ keepRatio: false,
3772
+ // Pintura-style solid round corner dots
3773
+ enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
3774
+ anchorSize: 14,
3775
+ anchorCornerRadius: 7,
3776
+ anchorFill: "#111",
3777
+ anchorStroke: "#111",
3778
+ anchorStrokeWidth: 1,
3779
+ borderStroke: "rgba(255, 255, 255, 0.7)",
3780
+ boundBoxFunc: (oldBox, newBox) => ({ ...newBox, ...clampCropBox(imageBounds, oldBox, newBox, ratioLocked) })
3781
+ });
3782
+ layer.add(transformer);
3783
+ cropNode.dragBoundFunc((position) => clampToImage({
3784
+ ...position,
3785
+ width: cropNode.width() * cropNode.scaleX(),
3786
+ height: cropNode.height() * cropNode.scaleY()
3787
+ }));
3788
+ cropNode.on("dragmove transform", updateOverlay);
3789
+ const setAspect = (aspect) => {
3790
+ ratioLocked = aspect !== null;
3791
+ transformer.keepRatio(ratioLocked);
3792
+ if (aspect === null) {
3793
+ return;
3794
+ }
3795
+ const current = stageRect();
3796
+ const width = Math.min(
3797
+ Math.max(current.width, current.height * aspect),
3798
+ imageBounds.width,
3799
+ imageBounds.height * aspect
3800
+ );
3801
+ const height = width / aspect;
3802
+ const center = { x: current.x + current.width / 2, y: current.y + current.height / 2 };
3803
+ const position = clampToImage({
3804
+ x: center.x - width / 2,
3805
+ y: center.y - height / 2,
3806
+ width,
3807
+ height
3808
+ });
3809
+ cropNode.setAttrs({ ...position, width, height, scaleX: 1, scaleY: 1 });
3810
+ transformer.forceUpdate();
3811
+ updateOverlay();
3812
+ };
3813
+ updateOverlay();
3814
+ deps.stage.add(layer);
3815
+ return {
3816
+ layer,
3817
+ setAspect,
3818
+ update(next) {
3819
+ const scene = toScene(stageRect());
3820
+ view.oriented = next.oriented;
3821
+ view.scale = next.scale;
3822
+ view.offset = next.offset;
3823
+ imageBounds = toStage({ x: 0, y: 0, ...view.oriented });
3824
+ cropNode.setAttrs({ ...toStage(scene), scaleX: 1, scaleY: 1 });
3825
+ transformer.forceUpdate();
3826
+ updateOverlay();
3827
+ },
3828
+ getRect() {
3829
+ const scene = toScene(stageRect());
3830
+ return clampRect({
3831
+ x: Math.round(scene.x),
3832
+ y: Math.round(scene.y),
3833
+ width: Math.round(scene.width),
3834
+ height: Math.round(scene.height)
3835
+ }, view.oriented);
3836
+ },
3837
+ destroy() {
3838
+ transformer.destroy();
3839
+ layer.destroy();
3840
+ }
3841
+ };
3842
+ }
3843
+ function coverScale(box, degrees) {
3844
+ if (box.width <= 0 || box.height <= 0) {
3845
+ throw new RangeError("Dimensions must be positive");
3846
+ }
3847
+ const radians = degrees * Math.PI / 180;
3848
+ const cos = Math.abs(Math.cos(radians));
3849
+ const sin = Math.abs(Math.sin(radians));
3850
+ return Math.max(
3851
+ (box.width * cos + box.height * sin) / box.width,
3852
+ (box.width * sin + box.height * cos) / box.height
3853
+ );
3854
+ }
3855
+ function fitContain(content, container) {
3856
+ if (content.width <= 0 || content.height <= 0 || container.width <= 0 || container.height <= 0) {
3857
+ throw new RangeError("Dimensions must be positive");
3858
+ }
3859
+ const scale = Math.min(
3860
+ container.width / content.width,
3861
+ container.height / content.height,
3862
+ 1
3863
+ );
3864
+ const width = content.width * scale;
3865
+ const height = content.height * scale;
3866
+ return {
3867
+ scale,
3868
+ width,
3869
+ height,
3870
+ x: (container.width - width) / 2,
3871
+ y: (container.height - height) / 2
3872
+ };
3873
+ }
3874
+ function orientImage(image, state) {
3875
+ const natural = { width: image.naturalWidth, height: image.naturalHeight };
3876
+ const oriented = orientedSize(natural, state.rotation);
3877
+ const canvas = document.createElement("canvas");
3878
+ canvas.width = oriented.width;
3879
+ canvas.height = oriented.height;
3880
+ const context = canvas.getContext("2d");
3881
+ if (context === null) {
3882
+ throw new Error("Canvas 2D context unavailable");
3883
+ }
3884
+ const cover = coverScale(oriented, state.fineRotation) * Math.max(1, state.zoom);
3885
+ context.translate(oriented.width / 2, oriented.height / 2);
3886
+ context.rotate(state.fineRotation * Math.PI / 180);
3887
+ context.scale(cover, cover);
3888
+ context.rotate(state.rotation * Math.PI / 180);
3889
+ context.scale(state.flipX ? -1 : 1, state.flipY ? -1 : 1);
3890
+ context.drawImage(image, -natural.width / 2, -natural.height / 2);
3891
+ return canvas;
3892
+ }
3893
+ function transformPoints(points, node) {
3894
+ const radians = node.rotation() * Math.PI / 180;
3895
+ const cos = Math.cos(radians);
3896
+ const sin = Math.sin(radians);
3897
+ const result = [];
3898
+ for (let i = 0; i < points.length; i += 2) {
3899
+ const x = points[i] * node.scaleX();
3900
+ const y = points[i + 1] * node.scaleY();
3901
+ result.push(node.x() + x * cos - y * sin, node.y() + x * sin + y * cos);
3902
+ }
3903
+ return result;
3904
+ }
3905
+ function applyNodeTransform(annotation, node) {
3906
+ switch (annotation.type) {
3907
+ case "draw":
3908
+ case "arrow": {
3909
+ const points = transformPoints(annotation.points, node);
3910
+ const scale = (Math.abs(node.scaleX()) + Math.abs(node.scaleY())) / 2;
3911
+ const strokeWidth = Math.max(1, annotation.strokeWidth * scale);
3912
+ return { ...annotation, points, strokeWidth };
3913
+ }
3914
+ case "redact":
3915
+ return {
3916
+ ...annotation,
3917
+ rect: {
3918
+ x: node.x(),
3919
+ y: node.y(),
3920
+ width: Math.max(1, annotation.rect.width * node.scaleX()),
3921
+ height: Math.max(1, annotation.rect.height * node.scaleY())
3922
+ }
3923
+ };
3924
+ case "rectangle":
3925
+ case "ellipse":
3926
+ return {
3927
+ ...annotation,
3928
+ rect: {
3929
+ x: node.x(),
3930
+ y: node.y(),
3931
+ width: Math.max(1, annotation.rect.width * node.scaleX()),
3932
+ height: Math.max(1, annotation.rect.height * node.scaleY())
3933
+ },
3934
+ rotation: node.rotation()
3935
+ };
3936
+ case "text":
3937
+ case "sticker":
3938
+ return {
3939
+ ...annotation,
3940
+ x: node.x(),
3941
+ y: node.y(),
3942
+ fontSize: Math.max(4, annotation.fontSize * node.scaleY()),
3943
+ rotation: node.rotation()
3944
+ };
3945
+ }
3946
+ }
3947
+ function attachSelection(deps) {
3948
+ const accent = primaryColor();
3949
+ const transformer = new Konva__default.default.Transformer({
3950
+ rotateEnabled: true,
3951
+ flipEnabled: false,
3952
+ ignoreStroke: true,
3953
+ // A plain square with four corner handles
3954
+ enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
3955
+ anchorSize: 12,
3956
+ anchorCornerRadius: 6,
3957
+ anchorFill: "#fff",
3958
+ anchorStroke: accent,
3959
+ borderStroke: accent,
3960
+ rotateAnchorOffset: 24,
3961
+ rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315],
3962
+ rotationSnapTolerance: 6
3963
+ });
3964
+ const layer = new Konva__default.default.Layer({ name: "selection" });
3965
+ layer.add(transformer);
3966
+ deps.stage.add(layer);
3967
+ const findAnnotation = (id) => deps.getState().annotations.find((annotation) => annotation.id === id);
3968
+ const findNode = (id) => id === null ? null : deps.stage.find(".annotation").find((node) => node.id() === id) ?? null;
3969
+ const reportRect = () => {
3970
+ const node = findNode(deps.getSelectedId());
3971
+ deps.onSelectionRect(node ? node.getClientRect() : null);
3972
+ };
3973
+ const syncTransformer = () => {
3974
+ const id = deps.getSelectedId();
3975
+ const node = findNode(id);
3976
+ if (node === null || node === void 0) {
3977
+ transformer.nodes([]);
3978
+ deps.onSelectionRect(null);
3979
+ return;
3980
+ }
3981
+ const annotation = findAnnotation(id);
3982
+ transformer.rotateEnabled(annotation !== void 0 && annotation.type !== "redact");
3983
+ transformer.nodes([node]);
3984
+ deps.onSelectionRect(node.getClientRect());
3985
+ };
3986
+ const sync = () => {
3987
+ deps.stage.find(".annotation").forEach((node) => node.draggable(true));
3988
+ syncTransformer();
3989
+ };
3990
+ sync();
3991
+ const onWriteBack = (event) => {
3992
+ const node = event.target;
3993
+ const annotation = findAnnotation(node.id());
3994
+ if (annotation === void 0) {
3995
+ return;
3996
+ }
3997
+ const state = deps.getState();
3998
+ deps.commit({
3999
+ ...state,
4000
+ annotations: state.annotations.map((entry) => entry.id === annotation.id ? applyNodeTransform(annotation, node) : entry)
4001
+ }, t("Move or resize"));
4002
+ };
4003
+ const onClick = (event) => {
4004
+ if (event.target.hasName("annotation")) {
4005
+ deps.select(event.target.id());
4006
+ } else if (event.target === deps.stage || event.target instanceof Konva__default.default.Image) {
4007
+ deps.select(null);
4008
+ }
4009
+ syncTransformer();
4010
+ };
4011
+ const onDblClick = (event) => {
4012
+ const annotation = findAnnotation(event.target.id());
4013
+ if (annotation?.type === "text") {
4014
+ deps.editText(annotation);
4015
+ }
4016
+ };
4017
+ deps.stage.on("click.selection tap.selection", onClick);
4018
+ deps.stage.on("dblclick.selection dbltap.selection", onDblClick);
4019
+ deps.stage.on("dragend.selection transformend.selection", onWriteBack);
4020
+ deps.stage.on("dragmove.selection transform.selection", reportRect);
4021
+ return {
4022
+ sync,
4023
+ detach() {
4024
+ deps.stage.off(".selection");
4025
+ deps.stage.find(".annotation").forEach((node) => node.draggable(false));
4026
+ deps.onSelectionRect(null);
4027
+ transformer.destroy();
4028
+ layer.destroy();
4029
+ }
4030
+ };
4031
+ }
4032
+ const TOOL_LABELS = {
4033
+ draw: t("Draw"),
4034
+ rectangle: t("Rectangle"),
4035
+ ellipse: t("Ellipse"),
4036
+ arrow: t("Arrow"),
4037
+ sticker: t("Sticker"),
4038
+ redact: t("Redact")
4039
+ };
4040
+ function attachPointerTools(tool, deps) {
4041
+ if (!["draw", "rectangle", "ellipse", "arrow", "text", "sticker", "redact"].includes(tool)) {
4042
+ return () => {
4043
+ };
4044
+ }
4045
+ let active = null;
4046
+ let previewNode = null;
4047
+ let start = { x: 0, y: 0 };
4048
+ let pendingText = null;
4049
+ const scenePointer = () => {
4050
+ const pointer = deps.stage.getPointerPosition();
4051
+ return pointer === null ? null : deps.toScene(pointer);
4052
+ };
4053
+ const refreshPreview = () => {
4054
+ if (active === null) {
4055
+ previewNode?.destroy();
4056
+ previewNode = null;
4057
+ return;
4058
+ }
4059
+ if (previewNode !== null && (active.type === "draw" || active.type === "arrow")) {
4060
+ previewNode.points(active.points);
4061
+ return;
4062
+ }
4063
+ previewNode?.destroy();
4064
+ previewNode = null;
4065
+ const source = deps.oriented() ?? void 0;
4066
+ if (active.type === "redact" && source === void 0) {
4067
+ return;
4068
+ }
4069
+ previewNode = buildAnnotationNode(active, source);
4070
+ deps.contentGroup()?.add(previewNode);
4071
+ };
4072
+ const discard = () => {
4073
+ active = null;
4074
+ pendingText = null;
4075
+ previewNode?.destroy();
4076
+ previewNode = null;
4077
+ };
4078
+ const onPointerDown = () => {
4079
+ const point = scenePointer();
4080
+ if (point === null || deps.panning()) {
4081
+ return;
4082
+ }
4083
+ const options = deps.options();
4084
+ start = point;
4085
+ switch (tool) {
4086
+ case "draw":
4087
+ active = { id: newId(), type: "draw", points: [point.x, point.y], color: options.color, strokeWidth: options.strokeWidth };
4088
+ break;
4089
+ case "arrow":
4090
+ active = { id: newId(), type: "arrow", points: [point.x, point.y, point.x, point.y], color: options.color, strokeWidth: options.strokeWidth };
4091
+ break;
4092
+ case "rectangle":
4093
+ case "ellipse":
4094
+ active = { id: newId(), type: tool, rect: { x: point.x, y: point.y, width: 1, height: 1 }, rotation: 0, color: options.color, strokeWidth: options.strokeWidth };
4095
+ break;
4096
+ case "redact":
4097
+ active = { id: newId(), type: "redact", rect: { x: point.x, y: point.y, width: 1, height: 1 }, style: options.redactStyle };
4098
+ break;
4099
+ case "text":
4100
+ pendingText = point;
4101
+ return;
4102
+ case "sticker": {
4103
+ const state = deps.getState();
4104
+ const sticker = {
4105
+ id: newId(),
4106
+ type: "sticker",
4107
+ x: point.x,
4108
+ y: point.y,
4109
+ text: options.sticker,
4110
+ color: options.color,
4111
+ fontSize: options.fontSize * 2,
4112
+ rotation: 0
4113
+ };
4114
+ deps.commit({ ...state, annotations: [...state.annotations, sticker] }, TOOL_LABELS.sticker);
4115
+ return;
4116
+ }
4117
+ }
4118
+ refreshPreview();
4119
+ };
4120
+ const onPointerMove = () => {
4121
+ if (active === null) {
4122
+ return;
4123
+ }
4124
+ if (deps.panning()) {
4125
+ discard();
4126
+ return;
4127
+ }
4128
+ const point = scenePointer();
4129
+ if (point === null) {
4130
+ return;
4131
+ }
4132
+ switch (active.type) {
4133
+ case "draw":
4134
+ active.points.push(point.x, point.y);
4135
+ break;
4136
+ case "arrow":
4137
+ active.points = [start.x, start.y, point.x, point.y];
4138
+ break;
4139
+ case "rectangle":
4140
+ case "ellipse":
4141
+ case "redact":
4142
+ active = {
4143
+ ...active,
4144
+ rect: {
4145
+ x: Math.min(start.x, point.x),
4146
+ y: Math.min(start.y, point.y),
4147
+ width: Math.abs(point.x - start.x) || 1,
4148
+ height: Math.abs(point.y - start.y) || 1
4149
+ }
4150
+ };
4151
+ break;
4152
+ }
4153
+ refreshPreview();
4154
+ };
4155
+ const onPointerUp = () => {
4156
+ if (pendingText !== null) {
4157
+ deps.startTextEdit(pendingText);
4158
+ pendingText = null;
4159
+ return;
4160
+ }
4161
+ if (active === null) {
4162
+ return;
4163
+ }
4164
+ const state = deps.getState();
4165
+ deps.commit({ ...state, annotations: [...state.annotations, active] }, TOOL_LABELS[tool]);
4166
+ discard();
4167
+ };
4168
+ deps.stage.on("pointerdown.tool", onPointerDown);
4169
+ deps.stage.on("pointermove.tool", onPointerMove);
4170
+ deps.stage.on("pointerup.tool", onPointerUp);
4171
+ window.addEventListener("pointerup", onPointerUp);
4172
+ window.addEventListener("pointercancel", discard);
4173
+ return () => {
4174
+ deps.stage.off(".tool");
4175
+ window.removeEventListener("pointerup", onPointerUp);
4176
+ window.removeEventListener("pointercancel", discard);
4177
+ previewNode?.destroy();
4178
+ };
4179
+ }
4180
+ const _hoisted_1 = { class: "image-editor__shell" };
4181
+ const _hoisted_2 = { class: "image-editor__frame" };
4182
+ const _hoisted_3 = { class: "image-editor__viewport" };
4183
+ const _hoisted_4 = ["aria-label"];
4184
+ const _hoisted_5 = {
4185
+ key: 1,
4186
+ class: "image-editor__error",
4187
+ "data-test": "load-error"
4188
+ };
4189
+ const _hoisted_6 = {
4190
+ class: "hidden-visually",
4191
+ role: "status",
4192
+ "aria-live": "polite"
4193
+ };
4194
+ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
4195
+ __name: "ImageEditor",
4196
+ props: {
4197
+ src: {},
4198
+ label: {},
4199
+ exportOptions: {},
4200
+ initialState: {}
4201
+ },
4202
+ emits: ["save", "cancel", "error", "change"],
4203
+ setup(__props, { expose: __expose, emit: __emit }) {
4204
+ const props = __props;
4205
+ const emit = __emit;
4206
+ const labels = {
4207
+ canvas: t("Image editor"),
4208
+ failed: t("The image could not be loaded"),
4209
+ retry: t("Try again")
4210
+ };
4211
+ let loadAttempt = 0;
4212
+ const context = createEditorContext();
4213
+ const container = vue.useTemplateRef("container");
4214
+ const loaded = vue.ref(false);
4215
+ const errored = vue.ref(false);
4216
+ const containerSize = vue.shallowRef({ width: 0, height: 0 });
4217
+ const orientedCanvas = vue.shallowRef(null);
4218
+ const sourceImage = vue.shallowRef(null);
4219
+ const { ambient, backdrop } = useAmbient(sourceImage);
4220
+ const { panArmed } = useWheelControls(container, context);
4221
+ const announcement = useAnnouncements(context);
4222
+ const selectionBox = vue.shallowRef(null);
4223
+ let stage = null;
4224
+ let scene = null;
4225
+ let cropOverlay = null;
4226
+ let selection = null;
4227
+ let detachTool = null;
4228
+ let attached = null;
4229
+ let resizeObserver = null;
4230
+ let pendingTransition = null;
4231
+ let lastView = null;
4232
+ const canvasCursor = vue.computed(() => {
4233
+ if (context.panning.value) {
4234
+ return "grabbing";
4235
+ }
4236
+ if (panArmed.value) {
4237
+ return "grab";
4238
+ }
4239
+ const tool = context.activeTool.value;
4240
+ if (["draw", "rectangle", "ellipse", "arrow", "text", "sticker", "redact"].includes(tool)) {
4241
+ return "crosshair";
4242
+ }
4243
+ return "default";
4244
+ });
4245
+ const viewFit = vue.computed(() => {
4246
+ const oriented = orientedCanvas.value;
4247
+ if (oriented === null || containerSize.value.width === 0 || containerSize.value.height === 0) {
4248
+ return null;
4249
+ }
4250
+ const showCropped = context.activeTool.value !== "crop";
4251
+ const visible = showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0, width: oriented.width, height: oriented.height };
4252
+ const container2 = containerSize.value;
4253
+ const fit = fitContain(
4254
+ { width: visible.width, height: visible.height },
4255
+ {
4256
+ width: Math.max(1, container2.width - VIEW_MARGIN * 2),
4257
+ height: Math.max(1, container2.height - VIEW_MARGIN * 2)
4258
+ }
4259
+ );
4260
+ return { scale: fit.scale, visible, container: container2, showCropped };
4261
+ });
4262
+ const viewOptions = vue.computed(() => {
4263
+ const fit = viewFit.value;
4264
+ if (fit === null) {
4265
+ return null;
4266
+ }
4267
+ const scale = fit.scale * context.viewZoom.value;
4268
+ const pan = clampPan(context.viewPan.value, panBounds(fit.visible, scale, fit.container));
4269
+ return {
4270
+ scale,
4271
+ offset: {
4272
+ x: (fit.container.width - fit.visible.width * scale) / 2 + pan.x,
4273
+ y: (fit.container.height - fit.visible.height * scale) / 2 + pan.y
4274
+ },
4275
+ showCropped: fit.showCropped,
4276
+ fastFilters: context.interacting.value
4277
+ };
4278
+ });
4279
+ const { textEdit, startTextEdit, confirmTextEdit } = useTextEditing({
4280
+ context,
4281
+ viewOptions: () => viewOptions.value,
4282
+ oriented: () => orientedCanvas.value
4283
+ });
4284
+ const { exportImage, save: onSave } = useExportImage({
4285
+ oriented: () => orientedCanvas.value,
4286
+ getState: () => context.state.value,
4287
+ source: () => props.src instanceof Blob ? props.src : null,
4288
+ saveOptions: () => props.exportOptions ?? {},
4289
+ onSaved: (result) => emit("save", result),
4290
+ onError: (error) => emit("error", error)
4291
+ });
4292
+ function captureView() {
4293
+ return lastView ?? { previousScale: 1, previousOffset: { x: 0, y: 0 }, previousOrigin: { x: 0, y: 0 } };
4294
+ }
4295
+ function commitWithTransition(kind, next, label) {
4296
+ pendingTransition = { kind, context: captureView() };
4297
+ context.commit(next, label);
4298
+ }
4299
+ function renderView() {
4300
+ const oriented = orientedCanvas.value;
4301
+ const options = viewOptions.value;
4302
+ if (stage === null || oriented === null || options === null) {
4303
+ return;
4304
+ }
4305
+ stage.size(containerSize.value);
4306
+ scene ??= createScene(stage);
4307
+ scene.update(oriented, context.state.value, options);
4308
+ const renderedOrigin = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
4309
+ syncTools(oriented, options);
4310
+ if (pendingTransition !== null) {
4311
+ const visible = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0, width: oriented.width, height: oriented.height };
4312
+ const center = {
4313
+ x: visible.x + visible.width / 2,
4314
+ y: visible.y + visible.height / 2
4315
+ };
4316
+ const targets = [{
4317
+ node: scene.contentGroup,
4318
+ pivot: center,
4319
+ unit: 1 / options.scale
4320
+ }];
4321
+ if (cropOverlay !== null) {
4322
+ targets.push({
4323
+ node: cropOverlay.layer,
4324
+ pivot: {
4325
+ x: options.offset.x + (center.x - renderedOrigin.x) * options.scale,
4326
+ y: options.offset.y + (center.y - renderedOrigin.y) * options.scale
4327
+ },
4328
+ unit: 1
4329
+ });
4330
+ }
4331
+ playTransition(pendingTransition.kind, {
4332
+ targets,
4333
+ scale: options.scale,
4334
+ offset: options.offset,
4335
+ origin: renderedOrigin
4336
+ }, pendingTransition.context);
4337
+ pendingTransition = null;
4338
+ }
4339
+ lastView = {
4340
+ previousScale: options.scale,
4341
+ previousOffset: options.offset,
4342
+ previousOrigin: { x: renderedOrigin.x, y: renderedOrigin.y }
4343
+ };
4344
+ }
4345
+ function syncTools(oriented, options) {
4346
+ if (stage === null) {
4347
+ return;
4348
+ }
4349
+ const tool = context.activeTool.value;
4350
+ const crop = context.state.value.crop;
4351
+ const fresh = attached !== null && attached.tool === tool && attached.oriented === oriented && attached.crop === crop;
4352
+ if (fresh) {
4353
+ selection?.sync();
4354
+ cropOverlay?.update({
4355
+ oriented: { width: oriented.width, height: oriented.height },
4356
+ scale: options.scale,
4357
+ offset: options.offset
4358
+ });
4359
+ return;
4360
+ }
4361
+ detachTool?.();
4362
+ detachTool = null;
4363
+ selection?.detach();
4364
+ selection = null;
4365
+ cropOverlay?.destroy();
4366
+ cropOverlay = null;
4367
+ attached = { tool, oriented, crop };
4368
+ if (tool === "select") {
4369
+ selection = attachSelection({
4370
+ stage,
4371
+ getState: () => context.state.value,
4372
+ commit: context.commit,
4373
+ select: (id) => {
4374
+ context.selectedId.value = id;
4375
+ },
4376
+ getSelectedId: () => context.selectedId.value,
4377
+ editText: (annotation) => startTextEdit({ x: annotation.x, y: annotation.y }, annotation),
4378
+ onSelectionRect: (rect) => {
4379
+ selectionBox.value = rect;
4380
+ }
4381
+ });
4382
+ } else if (tool === "crop") {
4383
+ cropOverlay = attachCropOverlay({
4384
+ stage,
4385
+ oriented: { width: oriented.width, height: oriented.height },
4386
+ scale: options.scale,
4387
+ offset: options.offset,
4388
+ initial: crop
4389
+ });
4390
+ applyCropAspect();
4391
+ } else if (tool !== "adjust") {
4392
+ detachTool = attachPointerTools(tool, {
4393
+ stage,
4394
+ contentGroup: () => scene?.contentGroup ?? null,
4395
+ oriented: () => orientedCanvas.value,
4396
+ getState: () => context.state.value,
4397
+ commit: context.commit,
4398
+ // Read the live view: the tool outlives the transform it was
4399
+ // attached under
4400
+ toScene: (pointer) => toImageCoords(
4401
+ pointer,
4402
+ context.state.value,
4403
+ { width: oriented.width, height: oriented.height },
4404
+ viewOptions.value ?? options
4405
+ ),
4406
+ panning: () => context.panning.value,
4407
+ options: () => ({
4408
+ color: context.drawColor.value,
4409
+ strokeWidth: context.strokeWidth.value,
4410
+ fontSize: context.fontSize.value,
4411
+ sticker: context.sticker.value,
4412
+ redactStyle: context.redactStyle.value
4413
+ }),
4414
+ startTextEdit: (position) => startTextEdit(position)
4415
+ });
4416
+ }
4417
+ }
4418
+ function applyCropAspect() {
4419
+ if (cropOverlay === null) {
4420
+ return;
4421
+ }
4422
+ const aspect = context.cropAspect.value;
4423
+ const oriented = orientedCanvas.value;
4424
+ if (aspect === "original" && oriented !== null) {
4425
+ cropOverlay.setAspect(oriented.width / oriented.height);
4426
+ } else {
4427
+ cropOverlay.setAspect(typeof aspect === "number" ? aspect : null);
4428
+ }
4429
+ }
4430
+ function refreshOrientedCanvas() {
4431
+ if (sourceImage.value === null) {
4432
+ return;
4433
+ }
4434
+ orientedCanvas.value = orientImage(sourceImage.value, context.state.value);
4435
+ }
4436
+ function seedState(seed, image) {
4437
+ if (seed.crop === null) {
4438
+ return seed;
4439
+ }
4440
+ const oriented = orientedSize({ width: image.naturalWidth, height: image.naturalHeight }, seed.rotation);
4441
+ return { ...seed, crop: clampRect(seed.crop, oriented) };
4442
+ }
4443
+ async function load() {
4444
+ const attempt = ++loadAttempt;
4445
+ loaded.value = false;
4446
+ errored.value = false;
4447
+ try {
4448
+ const image = await loadImage(props.src);
4449
+ if (attempt !== loadAttempt) {
4450
+ return;
4451
+ }
4452
+ sourceImage.value = image;
4453
+ context.reset(props.initialState === void 0 ? void 0 : seedState(props.initialState, image));
4454
+ const minDimension = Math.min(image.naturalWidth, image.naturalHeight);
4455
+ context.fontSize.value = Math.min(128, Math.max(12, Math.round(minDimension / 15)));
4456
+ pendingTransition = { kind: "load", context: captureView() };
4457
+ refreshOrientedCanvas();
4458
+ loaded.value = true;
4459
+ } catch (error) {
4460
+ if (attempt !== loadAttempt) {
4461
+ return;
4462
+ }
4463
+ errored.value = true;
4464
+ emit("error", error instanceof Error ? error : new Error(String(error)));
4465
+ }
4466
+ }
4467
+ function currentOriented() {
4468
+ const oriented = orientedCanvas.value;
4469
+ return { width: oriented.width, height: oriented.height };
4470
+ }
4471
+ function onRotateCW() {
4472
+ commitWithTransition("rotate-cw", rotateCW(context.state.value, currentOriented()), t("Rotate right"));
4473
+ }
4474
+ function onRotateCCW() {
4475
+ let state = context.state.value;
4476
+ let oriented = currentOriented();
4477
+ for (let i = 0; i < 3; i++) {
4478
+ state = rotateCW(state, oriented);
4479
+ oriented = { width: oriented.height, height: oriented.width };
4480
+ }
4481
+ commitWithTransition("rotate-ccw", state, t("Rotate left"));
4482
+ }
4483
+ function onFlipHorizontal() {
4484
+ commitWithTransition("flip-h", flipHorizontal(context.state.value, currentOriented()), t("Flip horizontal"));
4485
+ }
4486
+ function onFlipVertical() {
4487
+ commitWithTransition("flip-v", flipVertical(context.state.value, currentOriented()), t("Flip vertical"));
4488
+ }
4489
+ function onApplyCrop() {
4490
+ if (cropOverlay !== null) {
4491
+ const crop = cropOverlay.getRect();
4492
+ pendingTransition = { kind: "crop", context: captureView() };
4493
+ context.commit({ ...context.state.value, crop }, t("Crop"));
4494
+ context.setMode("annotate");
4495
+ }
4496
+ }
4497
+ function onDuplicateSelection() {
4498
+ const id = context.selectedId.value;
4499
+ const state = context.state.value;
4500
+ const annotation = state.annotations.find((entry) => entry.id === id);
4501
+ if (annotation === void 0) {
4502
+ return;
4503
+ }
4504
+ const copy = duplicateAnnotation(annotation);
4505
+ context.commit({ ...state, annotations: [...state.annotations, copy] }, t("Duplicate"));
4506
+ context.selectedId.value = copy.id;
4507
+ renderView();
4508
+ }
4509
+ function onRevert() {
4510
+ context.commit(createInitialState(), t("Revert all changes"));
4511
+ }
4512
+ function onResetCrop() {
4513
+ context.commit({ ...context.state.value, crop: null }, t("Reset crop"));
4514
+ }
4515
+ function onDeleteSelection() {
4516
+ const id = context.selectedId.value;
4517
+ if (id === null) {
4518
+ return;
4519
+ }
4520
+ const state = context.state.value;
4521
+ context.selectedId.value = null;
4522
+ context.commit({
4523
+ ...state,
4524
+ annotations: state.annotations.filter((annotation) => annotation.id !== id)
4525
+ }, t("Delete"));
4526
+ }
4527
+ useEditorShortcuts({
4528
+ context,
4529
+ isTextEditing: () => textEdit.value !== null,
4530
+ onDelete: onDeleteSelection,
4531
+ onEscape: () => {
4532
+ if (context.selectedId.value !== null) {
4533
+ context.selectedId.value = null;
4534
+ renderView();
4535
+ } else if (context.activeMode.value === "crop") {
4536
+ context.setMode("select");
4537
+ } else if (context.activeMode.value === "annotate" && context.activeTool.value !== "select") {
4538
+ context.activeTool.value = "select";
4539
+ }
4540
+ }
4541
+ });
4542
+ vue.watch(viewFit, (fit) => {
4543
+ context.viewFit.value = fit;
4544
+ }, { immediate: true });
4545
+ provideEditorCommands({
4546
+ rotateCW: onRotateCW,
4547
+ rotateCCW: onRotateCCW,
4548
+ flipHorizontal: onFlipHorizontal,
4549
+ flipVertical: onFlipVertical,
4550
+ applyCrop: onApplyCrop,
4551
+ resetCrop: onResetCrop,
4552
+ revert: onRevert
4553
+ });
4554
+ vue.watch(() => props.src, load);
4555
+ vue.watch(
4556
+ [
4557
+ () => context.state.value.rotation,
4558
+ () => context.state.value.flipX,
4559
+ () => context.state.value.flipY,
4560
+ () => context.state.value.fineRotation,
4561
+ () => context.state.value.zoom
4562
+ ],
4563
+ refreshOrientedCanvas
4564
+ );
4565
+ vue.watch(
4566
+ [context.state, context.activeTool, context.activeMode, context.viewZoom, context.viewPan, context.interacting, orientedCanvas, containerSize],
4567
+ renderView
4568
+ );
4569
+ vue.watch(context.cropAspect, applyCropAspect);
4570
+ vue.watch(context.selectedId, (id) => {
4571
+ const annotation = context.state.value.annotations.find((entry) => entry.id === id);
4572
+ if (annotation !== void 0 && "color" in annotation && annotation.type !== "sticker") {
4573
+ context.drawColor.value = annotation.color;
4574
+ }
4575
+ });
4576
+ vue.watch([context.state, context.interacting], () => {
4577
+ if (!context.interacting.value) {
4578
+ emit("change", structuredClone(context.state.value));
4579
+ }
4580
+ });
4581
+ vue.onMounted(() => {
4582
+ stage = new Konva__default.default.Stage({ container: container.value, width: 1, height: 1 });
4583
+ resizeObserver = new ResizeObserver(() => {
4584
+ const { clientWidth, clientHeight } = container.value;
4585
+ containerSize.value = { width: clientWidth, height: clientHeight };
4586
+ });
4587
+ resizeObserver.observe(container.value);
4588
+ load();
4589
+ });
4590
+ vue.onBeforeUnmount(() => {
4591
+ resizeObserver?.disconnect();
4592
+ detachTool?.();
4593
+ selection?.detach();
4594
+ cropOverlay?.destroy();
4595
+ scene?.destroy();
4596
+ scene = null;
4597
+ stage?.destroy();
4598
+ stage = null;
4599
+ });
4600
+ __expose({
4601
+ exportImage,
4602
+ /**
4603
+ * Start over, optionally from a given state.
4604
+ *
4605
+ * @param state state to reset to, defaulting to a pristine one
4606
+ */
4607
+ reset: (state) => context.reset(state)
4608
+ });
4609
+ return (_ctx, _cache) => {
4610
+ return vue.openBlock(), vue.createElementBlock("div", {
4611
+ class: "image-editor",
4612
+ style: vue.normalizeStyle({
4613
+ "--editor-ambient": vue.unref(ambient),
4614
+ "--editor-backdrop": vue.unref(backdrop) ? `url(${vue.unref(backdrop)})` : "none"
4615
+ })
4616
+ }, [
4617
+ vue.createElementVNode("div", _hoisted_1, [
4618
+ vue.createElementVNode("div", _hoisted_2, [
4619
+ vue.createElementVNode("div", _hoisted_3, [
4620
+ vue.createElementVNode("div", {
4621
+ ref_key: "container",
4622
+ ref: container,
4623
+ class: "image-editor__canvas",
4624
+ style: vue.normalizeStyle({ cursor: canvasCursor.value }),
4625
+ role: "img",
4626
+ "aria-label": __props.label ?? labels.canvas
4627
+ }, null, 12, _hoisted_4),
4628
+ !loaded.value && !errored.value ? (vue.openBlock(), vue.createBlock(vue.unref(NcLoadingIcon__default.default), {
4629
+ key: 0,
4630
+ class: "image-editor__loading",
4631
+ size: 44
4632
+ })) : vue.createCommentVNode("", true),
4633
+ errored.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_5, [
4634
+ vue.createElementVNode("p", null, vue.toDisplayString(labels.failed), 1),
4635
+ vue.createVNode(vue.unref(NcButton__default.default), {
4636
+ variant: "secondary",
4637
+ "data-test": "retry",
4638
+ onClick: _cache[0] || (_cache[0] = ($event) => load())
4639
+ }, {
4640
+ default: vue.withCtx(() => [
4641
+ vue.createTextVNode(vue.toDisplayString(labels.retry), 1)
4642
+ ]),
4643
+ _: 1
4644
+ })
4645
+ ])) : vue.createCommentVNode("", true),
4646
+ vue.unref(textEdit) !== null ? (vue.openBlock(), vue.createBlock(TextOverlay, {
4647
+ key: 2,
4648
+ x: vue.unref(textEdit).screenX,
4649
+ y: vue.unref(textEdit).screenY,
4650
+ "font-size": vue.unref(textEdit).screenFontSize,
4651
+ color: vue.unref(textEdit).color,
4652
+ initial: vue.unref(textEdit).value,
4653
+ onConfirm: vue.unref(confirmTextEdit),
4654
+ onCancel: _cache[1] || (_cache[1] = ($event) => textEdit.value = null)
4655
+ }, null, 8, ["x", "y", "font-size", "color", "initial", "onConfirm"])) : vue.createCommentVNode("", true),
4656
+ selectionBox.value !== null ? (vue.openBlock(), vue.createBlock(SelectionToolbar, {
4657
+ key: 3,
4658
+ box: selectionBox.value,
4659
+ onDuplicate: onDuplicateSelection,
4660
+ onDelete: onDeleteSelection
4661
+ }, null, 8, ["box"])) : vue.createCommentVNode("", true)
4662
+ ]),
4663
+ vue.createVNode(EditorTopBar, {
4664
+ class: "image-editor__topbar",
4665
+ loaded: loaded.value,
4666
+ onSave: vue.unref(onSave),
4667
+ onCancel: _cache[2] || (_cache[2] = ($event) => emit("cancel"))
4668
+ }, null, 8, ["loaded", "onSave"]),
4669
+ vue.createVNode(EditorPanel, {
4670
+ class: vue.normalizeClass(vue.unref(context).activeMode.value === "filter" ? "image-editor__strip" : "image-editor__controls"),
4671
+ loaded: loaded.value,
4672
+ oriented: orientedCanvas.value
4673
+ }, null, 8, ["class", "loaded", "oriented"]),
4674
+ vue.createVNode(EditorSidebar, {
4675
+ class: "image-editor__rail",
4676
+ loaded: loaded.value
4677
+ }, null, 8, ["loaded"]),
4678
+ vue.createElementVNode("span", _hoisted_6, vue.toDisplayString(vue.unref(announcement)), 1)
4679
+ ])
4680
+ ])
4681
+ ], 4);
4682
+ };
4683
+ }
4684
+ });
4685
+ const ImageEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-e3346ccd"]]);
4686
+ exports.ImageEditor = ImageEditor;
4687
+ exports.createInitialState = createInitialState;
4688
+ exports.isPristine = isPristine;
4689
+ exports.useHistory = useHistory;
4690
+ //# sourceMappingURL=index.cjs.map