compote-ui 0.18.0 → 0.19.0

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,6 +7,10 @@ export interface ProcessImageOptions {
7
7
  maxHeight?: number;
8
8
  quality?: number;
9
9
  format?: 'image/webp' | 'image/jpeg' | 'image/png';
10
+ /** Trim near-white/near-transparent edges before resizing (default: false) */
11
+ trim?: boolean;
12
+ /** 0–255 tolerance for what counts as "white" (default: 10) */
13
+ trimThreshold?: number;
10
14
  }
11
15
  export interface CropRegion {
12
16
  x: number;
@@ -6,7 +6,9 @@ const defaults = {
6
6
  maxWidth: 1000,
7
7
  maxHeight: 1000,
8
8
  quality: 0.85,
9
- format: 'image/webp'
9
+ format: 'image/webp',
10
+ trim: false,
11
+ trimThreshold: 10
10
12
  };
11
13
  function canvasToBlob(canvas, format, quality) {
12
14
  return new Promise((resolve, reject) => {
@@ -18,6 +20,80 @@ function canvasToBlob(canvas, format, quality) {
18
20
  }, format, quality);
19
21
  });
20
22
  }
23
+ /** Returns the bounding box of non-white/non-transparent pixels */
24
+ function getTrimBounds(ctx, width, height, threshold) {
25
+ const { data } = ctx.getImageData(0, 0, width, height);
26
+ function isBackground(i) {
27
+ const a = data[i + 3];
28
+ if (a < threshold)
29
+ return true; // transparent
30
+ const r = data[i];
31
+ const g = data[i + 1];
32
+ const b = data[i + 2];
33
+ return r >= 255 - threshold && g >= 255 - threshold && b >= 255 - threshold;
34
+ }
35
+ let top = 0;
36
+ outer: for (let y = 0; y < height; y++) {
37
+ for (let x = 0; x < width; x++) {
38
+ if (!isBackground((y * width + x) * 4)) {
39
+ top = y;
40
+ break outer;
41
+ }
42
+ }
43
+ }
44
+ let bottom = height - 1;
45
+ outer: for (let y = height - 1; y >= 0; y--) {
46
+ for (let x = 0; x < width; x++) {
47
+ if (!isBackground((y * width + x) * 4)) {
48
+ bottom = y;
49
+ break outer;
50
+ }
51
+ }
52
+ }
53
+ let left = 0;
54
+ outer: for (let x = 0; x < width; x++) {
55
+ for (let y = top; y <= bottom; y++) {
56
+ if (!isBackground((y * width + x) * 4)) {
57
+ left = x;
58
+ break outer;
59
+ }
60
+ }
61
+ }
62
+ let right = width - 1;
63
+ outer: for (let x = width - 1; x >= 0; x--) {
64
+ for (let y = top; y <= bottom; y++) {
65
+ if (!isBackground((y * width + x) * 4)) {
66
+ right = x;
67
+ break outer;
68
+ }
69
+ }
70
+ }
71
+ return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
72
+ }
73
+ function applyTrim(sourceCanvas, threshold) {
74
+ const ctx = sourceCanvas.getContext('2d');
75
+ const { x, y, width, height } = getTrimBounds(ctx, sourceCanvas.width, sourceCanvas.height, threshold);
76
+ const trimmed = document.createElement('canvas');
77
+ trimmed.width = width;
78
+ trimmed.height = height;
79
+ trimmed.getContext('2d').drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height);
80
+ return trimmed;
81
+ }
82
+ function scaleCanvas(src, maxWidth, maxHeight) {
83
+ let { width, height } = src;
84
+ if (width > maxWidth || height > maxHeight) {
85
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
86
+ width = Math.round(width * ratio);
87
+ height = Math.round(height * ratio);
88
+ }
89
+ if (width === src.width && height === src.height)
90
+ return src;
91
+ const scaled = document.createElement('canvas');
92
+ scaled.width = width;
93
+ scaled.height = height;
94
+ scaled.getContext('2d').drawImage(src, 0, 0, width, height);
95
+ return scaled;
96
+ }
21
97
  /** Load an image element from a src URL (data URL, blob URL, or regular URL) */
22
98
  export function loadImage(src) {
23
99
  return new Promise((resolve, reject) => {
@@ -42,35 +118,27 @@ export function fileToDataUrl(file) {
42
118
  * Use this instead of getCroppedImage() from Ark UI which outputs at CSS/display resolution.
43
119
  */
44
120
  export async function cropImage(src, crop, opts) {
45
- const { maxWidth, maxHeight, quality, format } = { ...defaults, ...opts };
121
+ const { maxWidth, maxHeight, quality, format, trim, trimThreshold } = { ...defaults, ...opts };
46
122
  const img = await loadImage(src);
47
- let { width, height } = crop;
48
- if (width > maxWidth || height > maxHeight) {
49
- const ratio = Math.min(maxWidth / width, maxHeight / height);
50
- width = Math.round(width * ratio);
51
- height = Math.round(height * ratio);
52
- }
53
- const canvas = document.createElement('canvas');
54
- canvas.width = width;
55
- canvas.height = height;
56
- const ctx = canvas.getContext('2d');
57
- ctx.drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, width, height);
123
+ let canvas = document.createElement('canvas');
124
+ canvas.width = crop.width;
125
+ canvas.height = crop.height;
126
+ canvas.getContext('2d').drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);
127
+ if (trim)
128
+ canvas = applyTrim(canvas, trimThreshold);
129
+ canvas = scaleCanvas(canvas, maxWidth, maxHeight);
58
130
  return canvasToBlob(canvas, format, quality);
59
131
  }
60
132
  /** Resize and convert an image without cropping, returns a Blob */
61
133
  export async function processImage(src, opts) {
62
- const { maxWidth, maxHeight, quality, format } = { ...defaults, ...opts };
134
+ const { maxWidth, maxHeight, quality, format, trim, trimThreshold } = { ...defaults, ...opts };
63
135
  const img = await loadImage(src);
64
- let { width, height } = img;
65
- if (width > maxWidth || height > maxHeight) {
66
- const ratio = Math.min(maxWidth / width, maxHeight / height);
67
- width = Math.round(width * ratio);
68
- height = Math.round(height * ratio);
69
- }
70
- const canvas = document.createElement('canvas');
71
- canvas.width = width;
72
- canvas.height = height;
73
- const ctx = canvas.getContext('2d');
74
- ctx.drawImage(img, 0, 0, width, height);
136
+ let canvas = document.createElement('canvas');
137
+ canvas.width = img.naturalWidth;
138
+ canvas.height = img.naturalHeight;
139
+ canvas.getContext('2d').drawImage(img, 0, 0);
140
+ if (trim)
141
+ canvas = applyTrim(canvas, trimThreshold);
142
+ canvas = scaleCanvas(canvas, maxWidth, maxHeight);
75
143
  return canvasToBlob(canvas, format, quality);
76
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",