Skip to content

Commit 5c3f93a

Browse files
author
Felipe Lang
committed
fix: use image's MIME type instead of default image/png
Close #23
1 parent a226022 commit 5c3f93a

1 file changed

Lines changed: 69 additions & 5 deletions

File tree

src/main/resources/META-INF/resources/frontend/src/image-crop.tsx

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { type Crop, ReactCrop, PixelCrop, PercentCrop, makeAspectCrop, centerCro
2525

2626
class ImageCropElement extends ReactAdapterElement {
2727

28+
// Cache detected MIME per source URL to avoid repeated network calls
29+
#mimeTypeCache = new Map<string, string | null>();
30+
2831
protected render(hooks: RenderHooks): ReactElement<any, string | JSXElementConstructor<any>> | null {
2932

3033
const [crop, setCrop] = hooks.useState<Crop>("crop");
@@ -70,7 +73,7 @@ class ImageCropElement extends ReactAdapterElement {
7073
height
7174
)
7275
setCrop(newcrop);
73-
this._updateCroppedImage(newcrop);
76+
this._updateCroppedImage(newcrop).catch(console.error);
7477
}
7578
}
7679
};
@@ -123,7 +126,7 @@ class ImageCropElement extends ReactAdapterElement {
123126
};
124127

125128
const onComplete = (c: PixelCrop) => {
126-
this._updateCroppedImage(c);
129+
this._updateCroppedImage(c).catch(console.error);
127130
};
128131

129132
return (
@@ -160,11 +163,70 @@ class ImageCropElement extends ReactAdapterElement {
160163
})
161164
);
162165
}
166+
167+
/**
168+
* Attempts to detect the MIME type of given HTMLImageElement.
169+
*
170+
* Resolution order:
171+
* 1. If the image is a data URL, extracts the MIME type directly.
172+
* 2. Otherwise, sends a HEAD request to the image URL to read
173+
* the Content-Type header (faster, no body download).
174+
* 3. If the HEAD request does not provide Content-Type, falls back
175+
* to fetching the full image as a Blob and using `blob.type`.
176+
*
177+
* @param img The HTMLImageElement whose MIME type should be detected.
178+
* @returns A Promise resolving to the MIME type string (e.g. "image/png"),
179+
* or null if it cannot be determined.
180+
*/
181+
async #getImageMimeType(img: HTMLImageElement): Promise<string | null> {
182+
if (!img.src) {
183+
return null;
184+
}
185+
186+
// Return cached result if available
187+
const cacheKey = img.src;
188+
const cached = this.#mimeTypeCache?.get(cacheKey);
189+
if (cached !== undefined){
190+
return cached;
191+
}
192+
193+
// Case 1: data URL (e.g., data:image/png;base64,...)
194+
if (img.src.startsWith("data:")) {
195+
const semiIndex = img.src.indexOf(";");
196+
if (semiIndex > 5) {
197+
const mimeType = img.src.substring(5, semiIndex);
198+
this.#mimeTypeCache?.set(cacheKey, mimeType);
199+
return mimeType;
200+
}
201+
this.#mimeTypeCache?.set(cacheKey, null);
202+
return null;
203+
}
204+
205+
try {
206+
// Case 2: try a HEAD request (fast, no body)
207+
const headRes = await fetch(img.src, { method: "HEAD" });
208+
let mimeType = headRes.headers.get("Content-Type");
209+
if (mimeType) {
210+
this.#mimeTypeCache?.set(cacheKey, mimeType);
211+
return mimeType;
212+
}
213+
214+
// Case 3: fallback — fetch full blob
215+
const blobRes = await fetch(img.src);
216+
const blob = await blobRes.blob();
217+
mimeType = blob.type || null;
218+
this.#mimeTypeCache?.set(cacheKey, mimeType);
219+
return mimeType;
220+
} catch (err) {
221+
console.error("Error fetching image MIME type:", err);
222+
this.#mimeTypeCache?.set(cacheKey, null);
223+
return null;
224+
}
225+
}
163226

164-
public _updateCroppedImage(crop: PixelCrop|PercentCrop) {
227+
public async _updateCroppedImage(crop: PixelCrop|PercentCrop) {
165228
const image = this.querySelector("img");
166229
if (crop && image) {
167-
168230
crop = convertToPixelCrop(crop, image.width, image.height);
169231

170232
// create a canvas element to draw the cropped image
@@ -207,8 +269,10 @@ class ImageCropElement extends ReactAdapterElement {
207269

208270
ctx.restore();
209271

272+
const imgMimeType = await this.#getImageMimeType(image) || 'image/png';
273+
210274
// get the cropped image
211-
let croppedImageDataUri = canvas.toDataURL("image/png", 1.0);
275+
let croppedImageDataUri = canvas.toDataURL(imgMimeType, 1.0);
212276

213277
// dispatch the event containing cropped image
214278
this.fireCroppedImageEvent(croppedImageDataUri);

0 commit comments

Comments
 (0)