@@ -25,6 +25,9 @@ import { type Crop, ReactCrop, PixelCrop, PercentCrop, makeAspectCrop, centerCro
2525
2626class 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