@streamscloud/kit 0.1.11 → 0.1.12
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/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +6 -5
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +2 -2
- package/dist/ui/cropper/image-editor-dialog/index.js +1 -1
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +11 -3
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +1 -2
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +8 -11
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +19 -7
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +1 -0
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +23 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +9 -10
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +7 -19
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +10 -5
- package/dist/ui/cropper/img-cropper/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -7,11 +7,12 @@ import { untrack } from 'svelte';
|
|
|
7
7
|
const { controller, data } = $props();
|
|
8
8
|
const loc = new ImageEditorDialogLocalization();
|
|
9
9
|
const cropper = untrack(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
return new ImgCropper({
|
|
11
|
+
mode: data.mode ?? 'cover',
|
|
12
|
+
aspectRatio: data.aspectRatio,
|
|
13
|
+
fillColor: data.fillColor,
|
|
14
|
+
showImageShadow: data.showImageShadow ?? true
|
|
15
|
+
});
|
|
15
16
|
});
|
|
16
17
|
const url = $derived(data.url);
|
|
17
18
|
const save = async () => {
|
|
@@ -9,7 +9,7 @@ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types'
|
|
|
9
9
|
*
|
|
10
10
|
* ### Aspect ratio
|
|
11
11
|
* - `number` — fixed ratio, no dropdown.
|
|
12
|
-
* - `
|
|
12
|
+
* - `ImageEditorDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
|
|
13
13
|
* adds a free-ratio option; `initial` is optional when free crop is enabled.
|
|
14
14
|
* - Omitted — free crop (no ratio constraint on the selection).
|
|
15
15
|
*
|
|
@@ -18,4 +18,4 @@ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types'
|
|
|
18
18
|
* - `selectedRatio` — active aspect ratio at save time (`null` for free crop).
|
|
19
19
|
*/
|
|
20
20
|
export declare const openImageEditorDialog: (options: ImageEditorDialogOptions) => Promise<DialogResult<ImageEditorDialogResult>>;
|
|
21
|
-
export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
|
|
21
|
+
export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult, ImageEditorDynamicAspectRatio } from './types';
|
|
@@ -9,7 +9,7 @@ import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
|
|
|
9
9
|
*
|
|
10
10
|
* ### Aspect ratio
|
|
11
11
|
* - `number` — fixed ratio, no dropdown.
|
|
12
|
-
* - `
|
|
12
|
+
* - `ImageEditorDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
|
|
13
13
|
* adds a free-ratio option; `initial` is optional when free crop is enabled.
|
|
14
14
|
* - Omitted — free crop (no ratio constraint on the selection).
|
|
15
15
|
*
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { FileWithBlobUrl } from '../../../core/files';
|
|
2
|
-
|
|
2
|
+
export type ImageEditorDynamicAspectRatio = {
|
|
3
|
+
initial: number;
|
|
4
|
+
supported: number[];
|
|
5
|
+
allowFreeCrop?: false;
|
|
6
|
+
} | {
|
|
7
|
+
initial?: number;
|
|
8
|
+
supported: number[];
|
|
9
|
+
allowFreeCrop: true;
|
|
10
|
+
};
|
|
3
11
|
export type ImageEditorDialogOptions = {
|
|
4
12
|
url: string;
|
|
5
13
|
fillColor?: string;
|
|
6
14
|
showImageShadow?: boolean;
|
|
7
|
-
mode?:
|
|
8
|
-
aspectRatio?: number |
|
|
15
|
+
mode?: 'contain' | 'cover';
|
|
16
|
+
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
9
17
|
};
|
|
10
18
|
export type CroppedFile = FileWithBlobUrl & {
|
|
11
19
|
width: number;
|
|
@@ -69,8 +69,7 @@ const observeResize = (el) => {
|
|
|
69
69
|
style:height={cropper.canvasGeometry.height ? `${cropper.canvasGeometry.height}px` : undefined}
|
|
70
70
|
use:initCanvas
|
|
71
71
|
background={(cropper.showFillColor && cropper.isTransparentFill) || undefined}>
|
|
72
|
-
<cropper-image
|
|
73
|
-
</cropper-image>
|
|
72
|
+
<cropper-image initial-center-size={cropper.mode} scalable translatable rotatable> </cropper-image>
|
|
74
73
|
<cropper-shade hidden></cropper-shade>
|
|
75
74
|
<cropper-handle action="select" plain></cropper-handle>
|
|
76
75
|
<cropper-selection movable resizable aspect-ratio={cropper.aspectRatio ?? undefined}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CanvasGeometry, DragMode, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
1
|
+
import type { CanvasGeometry, DragMode, ImgCropperResult, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
2
2
|
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
3
3
|
export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
|
|
4
4
|
cropBoxVisible: boolean;
|
|
@@ -10,6 +10,7 @@ export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
|
|
|
10
10
|
protected _canvas: CropperCanvas;
|
|
11
11
|
protected _selectHandle: HTMLElement | null;
|
|
12
12
|
protected _originalSrc: string;
|
|
13
|
+
protected _sourceMime: string | null;
|
|
13
14
|
protected _visualWidth: number;
|
|
14
15
|
protected _visualHeight: number;
|
|
15
16
|
protected abstract readonly _centerMode: 'contain' | 'cover';
|
|
@@ -21,23 +22,19 @@ export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
|
|
|
21
22
|
reset: () => Promise<void>;
|
|
22
23
|
crop: (options?: {
|
|
23
24
|
fillColor?: string;
|
|
24
|
-
}) => Promise<
|
|
25
|
-
dataUrl: string;
|
|
26
|
-
width: number;
|
|
27
|
-
height: number;
|
|
28
|
-
}>;
|
|
25
|
+
}) => Promise<ImgCropperResult>;
|
|
29
26
|
save: (options?: {
|
|
30
27
|
fillColor?: string;
|
|
31
|
-
}) => Promise<
|
|
32
|
-
dataUrl: string;
|
|
33
|
-
width: number;
|
|
34
|
-
height: number;
|
|
35
|
-
}>;
|
|
28
|
+
}) => Promise<ImgCropperResult>;
|
|
36
29
|
clearSelection: () => void;
|
|
37
30
|
refit: () => Promise<void>;
|
|
38
31
|
enableCropMode: () => void;
|
|
39
32
|
protected _wrapTransformOp: (fn: () => void) => void;
|
|
40
33
|
protected _applyMoveMode: () => void;
|
|
34
|
+
protected _resolveOutputFormat: (fillColor: string | undefined) => {
|
|
35
|
+
mime: string;
|
|
36
|
+
quality: number;
|
|
37
|
+
};
|
|
41
38
|
protected _fitImage: () => Promise<void>;
|
|
42
39
|
protected abstract _computeCanvasSize(): void;
|
|
43
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ColorHelper } from '../../../core/utils';
|
|
2
2
|
import { computeImageScale, computeNaturalOutputSize, handleSelectionBoundary } from './img-cropper-utils';
|
|
3
3
|
import { tick } from 'svelte';
|
|
4
|
+
const OPAQUE_MIME_TYPES = new Set(['image/jpeg', 'image/bmp']);
|
|
4
5
|
export class ImgCropperBaseWorker {
|
|
5
6
|
cropBoxVisible = $state(false);
|
|
6
7
|
dragMode = $state('move');
|
|
@@ -11,10 +12,12 @@ export class ImgCropperBaseWorker {
|
|
|
11
12
|
_canvas;
|
|
12
13
|
_selectHandle;
|
|
13
14
|
_originalSrc;
|
|
15
|
+
_sourceMime;
|
|
14
16
|
_visualWidth = 0;
|
|
15
17
|
_visualHeight = 0;
|
|
16
18
|
constructor(params) {
|
|
17
19
|
this._originalSrc = params.originalSrc;
|
|
20
|
+
this._sourceMime = params.sourceMime;
|
|
18
21
|
this._image = params.image;
|
|
19
22
|
this._selection = params.selection;
|
|
20
23
|
this._canvas = params.canvas;
|
|
@@ -41,9 +44,7 @@ export class ImgCropperBaseWorker {
|
|
|
41
44
|
this._selection.$clear();
|
|
42
45
|
};
|
|
43
46
|
rotate = async () => {
|
|
44
|
-
|
|
45
|
-
this._visualWidth = this._visualHeight;
|
|
46
|
-
this._visualHeight = prevW;
|
|
47
|
+
[this._visualWidth, this._visualHeight] = [this._visualHeight, this._visualWidth];
|
|
47
48
|
this._computeCanvasSize();
|
|
48
49
|
await tick();
|
|
49
50
|
this._wrapTransformOp(() => {
|
|
@@ -77,12 +78,13 @@ export class ImgCropperBaseWorker {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
|
-
const
|
|
81
|
+
const format = this._resolveOutputFormat(fillColor);
|
|
82
|
+
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
81
83
|
this._applyMoveMode();
|
|
82
84
|
this._image.src = dataUrl;
|
|
83
85
|
await this._fitImage();
|
|
84
86
|
this._selection.$clear();
|
|
85
|
-
return { dataUrl, width: canvas.width, height: canvas.height };
|
|
87
|
+
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
86
88
|
};
|
|
87
89
|
save = async (options) => {
|
|
88
90
|
const fillColor = options?.fillColor;
|
|
@@ -114,8 +116,9 @@ export class ImgCropperBaseWorker {
|
|
|
114
116
|
// Reverse zoom shrinks image — must suspend cover check to avoid clamping
|
|
115
117
|
this._wrapTransformOp(() => this._image.$zoom(-0.003));
|
|
116
118
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
const format = this._resolveOutputFormat(fillColor);
|
|
120
|
+
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
121
|
+
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
119
122
|
};
|
|
120
123
|
clearSelection = () => {
|
|
121
124
|
this._selection.$clear();
|
|
@@ -149,6 +152,15 @@ export class ImgCropperBaseWorker {
|
|
|
149
152
|
this.dragMode = 'move';
|
|
150
153
|
this.cropBoxVisible = false;
|
|
151
154
|
};
|
|
155
|
+
_resolveOutputFormat = (fillColor) => {
|
|
156
|
+
if (this._sourceMime && OPAQUE_MIME_TYPES.has(this._sourceMime)) {
|
|
157
|
+
return { mime: 'image/jpeg', quality: 0.92 };
|
|
158
|
+
}
|
|
159
|
+
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
160
|
+
return { mime: 'image/jpeg', quality: 0.92 };
|
|
161
|
+
}
|
|
162
|
+
return { mime: 'image/png', quality: 1 };
|
|
163
|
+
};
|
|
152
164
|
_fitImage = async () => {
|
|
153
165
|
const img = await this._image.$ready();
|
|
154
166
|
this._visualWidth = img.naturalWidth;
|
|
@@ -5,6 +5,7 @@ type Rect = {
|
|
|
5
5
|
right: number;
|
|
6
6
|
bottom: number;
|
|
7
7
|
};
|
|
8
|
+
export declare const detectMimeFromUrl: (url: string) => string | null;
|
|
8
9
|
export declare const computeImageScale: (transform: number[]) => number;
|
|
9
10
|
export declare const computeNaturalOutputSize: (params: {
|
|
10
11
|
displayWidth: number;
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { Base64Helper } from '../../../core/files/base64-helper';
|
|
2
|
+
import { default as mime } from 'mime';
|
|
3
|
+
// Detect MIME type from a URL without fetching the resource.
|
|
4
|
+
// Returns a MIME string for data: URLs, extension-based URLs, or null for blob: / unknown URLs.
|
|
5
|
+
export const detectMimeFromUrl = (url) => {
|
|
6
|
+
if (url.startsWith('data:')) {
|
|
7
|
+
return Base64Helper.getMimeType(url);
|
|
8
|
+
}
|
|
9
|
+
if (url.startsWith('blob:')) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const pathname = url.includes('://') ? new URL(url).pathname : url.split('?')[0].split('#')[0];
|
|
14
|
+
const lastDot = pathname.lastIndexOf('.');
|
|
15
|
+
if (lastDot === -1) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return mime.getType(pathname.slice(lastDot + 1).toLowerCase()) ?? null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
1
24
|
// Extract the uniform scale factor from a CSS transform matrix [a, b, c, d, e, f].
|
|
2
25
|
// Works for any combination of scale + rotation: scale = sqrt(a² + b²).
|
|
3
26
|
export const computeImageScale = (transform) => {
|
|
@@ -7,12 +7,19 @@ export type CanvasGeometry = {
|
|
|
7
7
|
};
|
|
8
8
|
export type ImgCropperWorkerParams = {
|
|
9
9
|
originalSrc: string;
|
|
10
|
+
sourceMime: string | null;
|
|
10
11
|
image: CropperImage;
|
|
11
12
|
selection: CropperSelection;
|
|
12
13
|
canvas: CropperCanvas;
|
|
13
14
|
selectHandle: HTMLElement | null;
|
|
14
15
|
aspectRatio: number | null;
|
|
15
16
|
};
|
|
17
|
+
export type ImgCropperResult = {
|
|
18
|
+
dataUrl: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
mimeType: string;
|
|
22
|
+
};
|
|
16
23
|
export interface ImgCropperWorker {
|
|
17
24
|
cropBoxVisible: boolean;
|
|
18
25
|
dragMode: DragMode;
|
|
@@ -25,18 +32,10 @@ export interface ImgCropperWorker {
|
|
|
25
32
|
reset: () => Promise<void>;
|
|
26
33
|
crop: (options?: {
|
|
27
34
|
fillColor?: string;
|
|
28
|
-
}) => Promise<
|
|
29
|
-
dataUrl: string;
|
|
30
|
-
width: number;
|
|
31
|
-
height: number;
|
|
32
|
-
}>;
|
|
35
|
+
}) => Promise<ImgCropperResult>;
|
|
33
36
|
save: (options?: {
|
|
34
37
|
fillColor?: string;
|
|
35
|
-
}) => Promise<
|
|
36
|
-
dataUrl: string;
|
|
37
|
-
width: number;
|
|
38
|
-
height: number;
|
|
39
|
-
}>;
|
|
38
|
+
}) => Promise<ImgCropperResult>;
|
|
40
39
|
clearSelection: () => void;
|
|
41
40
|
enableCropMode: () => void;
|
|
42
41
|
refit: () => void | Promise<void>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { CanvasGeometry, DragMode } from './img-cropper-worker.svelte';
|
|
1
|
+
import type { CanvasGeometry, DragMode, ImgCropperResult } from './img-cropper-worker.svelte';
|
|
2
2
|
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
3
3
|
export type { CanvasGeometry };
|
|
4
4
|
export type CropperMode = 'contain' | 'cover';
|
|
5
|
-
export type CorsMode = 'native' | 'fetch';
|
|
6
5
|
export type { DragMode };
|
|
7
6
|
export type ImgCropperRatioOption = {
|
|
8
7
|
label: string;
|
|
@@ -17,26 +16,19 @@ export type ImgCropperDynamicAspectRatio = {
|
|
|
17
16
|
supported: number[];
|
|
18
17
|
allowFreeCrop: true;
|
|
19
18
|
};
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
height: number;
|
|
24
|
-
};
|
|
25
|
-
type ImgCropperOptionsBase = {
|
|
26
|
-
corsMode?: CorsMode;
|
|
27
|
-
fillColor?: string;
|
|
28
|
-
showImageShadow?: boolean;
|
|
29
|
-
};
|
|
30
|
-
export type ImgCropperOptions = ImgCropperOptionsBase & {
|
|
19
|
+
export type { ImgCropperResult };
|
|
20
|
+
export type ImgCropperSaveResult = ImgCropperResult;
|
|
21
|
+
export type ImgCropperOptions = {
|
|
31
22
|
mode: CropperMode;
|
|
32
23
|
aspectRatio?: number | ImgCropperDynamicAspectRatio;
|
|
24
|
+
fillColor?: string;
|
|
25
|
+
showImageShadow?: boolean;
|
|
33
26
|
};
|
|
34
27
|
export declare class ImgCropper {
|
|
35
28
|
loading: boolean;
|
|
36
29
|
ready: boolean;
|
|
37
30
|
fillColor: string;
|
|
38
31
|
readonly mode: CropperMode;
|
|
39
|
-
readonly corsMode: CorsMode;
|
|
40
32
|
readonly ratioOptions: ImgCropperRatioOption[] | null;
|
|
41
33
|
readonly showImageShadow: boolean;
|
|
42
34
|
private _worker;
|
|
@@ -62,11 +54,7 @@ export declare class ImgCropper {
|
|
|
62
54
|
zoomOut: () => void;
|
|
63
55
|
reset: () => Promise<void>;
|
|
64
56
|
changeAspectRatio: (ratio: number | null) => Promise<void>;
|
|
65
|
-
crop: () => Promise<
|
|
66
|
-
dataUrl: string;
|
|
67
|
-
width: number;
|
|
68
|
-
height: number;
|
|
69
|
-
} | null>;
|
|
57
|
+
crop: () => Promise<ImgCropperSaveResult | null>;
|
|
70
58
|
save: () => Promise<ImgCropperSaveResult | null>;
|
|
71
59
|
clearSelection: () => void;
|
|
72
60
|
enableCropMode: () => void;
|
|
@@ -2,12 +2,12 @@ import { FileWithBlobDataHelper } from '../../../core/files';
|
|
|
2
2
|
import { ColorHelper, Utils } from '../../../core/utils';
|
|
3
3
|
import { ImgCropperContainWorker } from './img-cropper-contain-worker.svelte';
|
|
4
4
|
import { ImgCropperCoverWorker } from './img-cropper-cover-worker.svelte';
|
|
5
|
+
import { detectMimeFromUrl } from './img-cropper-utils';
|
|
5
6
|
export class ImgCropper {
|
|
6
7
|
loading = $state(false);
|
|
7
8
|
ready = $state(false);
|
|
8
9
|
fillColor = $state('');
|
|
9
10
|
mode;
|
|
10
|
-
corsMode;
|
|
11
11
|
ratioOptions;
|
|
12
12
|
showImageShadow;
|
|
13
13
|
_worker = $state.raw(null);
|
|
@@ -15,7 +15,6 @@ export class ImgCropper {
|
|
|
15
15
|
_defaultGeometry;
|
|
16
16
|
constructor(options) {
|
|
17
17
|
this.mode = options.mode;
|
|
18
|
-
this.corsMode = options.corsMode ?? 'native';
|
|
19
18
|
this.fillColor = options.fillColor ?? '';
|
|
20
19
|
this.showImageShadow = options.showImageShadow ?? true;
|
|
21
20
|
const toOption = (r) => {
|
|
@@ -68,13 +67,19 @@ export class ImgCropper {
|
|
|
68
67
|
this.loading = true;
|
|
69
68
|
try {
|
|
70
69
|
let resolvedSrc = params.src;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let sourceMime = null;
|
|
71
|
+
const file = await FileWithBlobDataHelper.fromUrl(params.src);
|
|
72
|
+
if (file) {
|
|
73
|
+
resolvedSrc = file.blobUrl;
|
|
74
|
+
sourceMime = file.type || null;
|
|
74
75
|
this._originalSrc = resolvedSrc;
|
|
75
76
|
}
|
|
77
|
+
if (!sourceMime) {
|
|
78
|
+
sourceMime = detectMimeFromUrl(params.src);
|
|
79
|
+
}
|
|
76
80
|
const workerParams = {
|
|
77
81
|
originalSrc: resolvedSrc,
|
|
82
|
+
sourceMime,
|
|
78
83
|
image: params.cropperImage,
|
|
79
84
|
selection: params.cropperSelection,
|
|
80
85
|
canvas: params.cropperCanvas,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as ImgCropperControls } from './cmp.img-cropper-controls.svelte';
|
|
2
2
|
export { default as ImgCropperToolbar } from './cmp.img-cropper-toolbar.svelte';
|
|
3
3
|
export { default as ImgCropperView } from './cmp.img-cropper.svelte';
|
|
4
|
-
export { type CanvasGeometry, type
|
|
4
|
+
export { type CanvasGeometry, type CropperMode, type DragMode, ImgCropper, type ImgCropperDynamicAspectRatio, type ImgCropperOptions, type ImgCropperRatioOption, type ImgCropperSaveResult } from './img-cropper.svelte';
|