@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.
@@ -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
- const showImageShadow = data.showImageShadow ?? true;
11
- if (data.mode === 'contain') {
12
- return new ImgCropper({ mode: 'contain', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
13
- }
14
- return new ImgCropper({ mode: 'cover', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
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
- * - `ImgCropperDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
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
- * - `ImgCropperDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
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
- import type { CropperMode, ImgCropperDynamicAspectRatio } from '../img-cropper';
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?: CropperMode;
8
- aspectRatio?: number | ImgCropperDynamicAspectRatio;
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 crossorigin={cropper.corsMode === 'native' ? 'anonymous' : null} initial-center-size={cropper.mode} scalable translatable rotatable>
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
- const prevW = this._visualWidth;
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 dataUrl = canvas.toDataURL('image/png', 0.9);
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 dataUrl = canvas.toDataURL('image/png', 0.9);
118
- return { dataUrl, width: canvas.width, height: canvas.height };
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 ImgCropperSaveResult = {
21
- dataUrl: string;
22
- width: number;
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
- if (this.corsMode === 'fetch') {
72
- const file = await FileWithBlobDataHelper.fromUrl(params.src);
73
- resolvedSrc = file ? file.blobUrl : params.src;
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 CorsMode, type CropperMode, type DragMode, ImgCropper, type ImgCropperDynamicAspectRatio, type ImgCropperOptions, type ImgCropperRatioOption, type ImgCropperSaveResult } from './img-cropper.svelte';
4
+ export { type CanvasGeometry, type CropperMode, type DragMode, ImgCropper, type ImgCropperDynamicAspectRatio, type ImgCropperOptions, type ImgCropperRatioOption, type ImgCropperSaveResult } from './img-cropper.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",