@@ -184,4 +184,158 @@ export const validateFileSize = (file: File): FileSizeError | null => {
184184export const formatFileSizeError = ( error : FileSizeError ) : string => {
185185 const typeLabel = error . fileType === 'image' ? 'Image' : error . fileType === 'pdf' ? 'PDF' : 'Text file' ;
186186 return `${ typeLabel } "${ error . fileName } " is too large (${ formatFileSize ( error . fileSize ) } ). Maximum size is ${ formatFileSize ( error . maxSize ) } .` ;
187+ } ;
188+
189+ /**
190+ * Resize an image to have a maximum edge of 4096px and convert to WebP format
191+ * Returns the optimized image if it's smaller than the original, otherwise returns the original
192+ */
193+ export const resizeImage = async ( file : File ) : Promise < File > => {
194+ // Only process actual image files (not SVG)
195+ if ( ! file . type . startsWith ( 'image/' ) || file . type === 'image/svg+xml' ) {
196+ return file ;
197+ }
198+
199+ const MAX_EDGE = 4096 ;
200+ const WEBP_QUALITY = 0.8 ;
201+
202+ return new Promise ( ( resolve ) => {
203+ const img = new Image ( ) ;
204+ const url = URL . createObjectURL ( file ) ;
205+
206+ img . onload = async ( ) => {
207+ URL . revokeObjectURL ( url ) ;
208+
209+ let { width, height } = img ;
210+
211+ // Check if resizing is needed
212+ if ( width <= MAX_EDGE && height <= MAX_EDGE ) {
213+ // Image is already small enough, just try WebP conversion
214+ const canvas = document . createElement ( 'canvas' ) ;
215+ canvas . width = width ;
216+ canvas . height = height ;
217+ const ctx = canvas . getContext ( '2d' ) ;
218+ ctx ?. drawImage ( img , 0 , 0 ) ;
219+
220+ canvas . toBlob (
221+ ( blob ) => {
222+ if ( blob && blob . size < file . size ) {
223+ const webpFile = new File ( [ blob ] , file . name . replace ( / \. [ ^ . ] + $ / , '.webp' ) , {
224+ type : 'image/webp' ,
225+ } ) ;
226+ console . log ( `Image resized (no dimension change): ${ file . name } - Original: ${ formatFileSize ( file . size ) } , WebP: ${ formatFileSize ( blob . size ) } ` ) ;
227+ resolve ( webpFile ) ;
228+ } else {
229+ console . log ( `Image kept original (WebP not smaller): ${ file . name } - ${ formatFileSize ( file . size ) } ` ) ;
230+ resolve ( file ) ;
231+ }
232+ } ,
233+ 'image/webp' ,
234+ WEBP_QUALITY
235+ ) ;
236+ return ;
237+ }
238+
239+ // Calculate new dimensions while maintaining aspect ratio
240+ if ( width > height ) {
241+ height = Math . round ( ( height * MAX_EDGE ) / width ) ;
242+ width = MAX_EDGE ;
243+ } else {
244+ width = Math . round ( ( width * MAX_EDGE ) / height ) ;
245+ height = MAX_EDGE ;
246+ }
247+
248+ // Create canvas and resize
249+ const canvas = document . createElement ( 'canvas' ) ;
250+ canvas . width = width ;
251+ canvas . height = height ;
252+ const ctx = canvas . getContext ( '2d' ) ;
253+ ctx ?. drawImage ( img , 0 , 0 , width , height ) ;
254+
255+ // Convert to WebP
256+ canvas . toBlob (
257+ ( blob ) => {
258+ if ( blob && blob . size < file . size ) {
259+ const webpFile = new File ( [ blob ] , file . name . replace ( / \. [ ^ . ] + $ / , '.webp' ) , {
260+ type : 'image/webp' ,
261+ } ) ;
262+ console . log ( `Image resized: ${ file . name } (${ img . width } x${ img . height } → ${ width } x${ height } ) - Original: ${ formatFileSize ( file . size ) } , WebP: ${ formatFileSize ( blob . size ) } ` ) ;
263+ resolve ( webpFile ) ;
264+ } else {
265+ console . log ( `Image kept original (WebP not smaller): ${ file . name } (${ img . width } x${ img . height } → ${ width } x${ height } ) - ${ formatFileSize ( file . size ) } ` ) ;
266+ resolve ( file ) ;
267+ }
268+ } ,
269+ 'image/webp' ,
270+ WEBP_QUALITY
271+ ) ;
272+ } ;
273+
274+ img . onerror = ( ) => {
275+ URL . revokeObjectURL ( url ) ;
276+ resolve ( file ) ;
277+ } ;
278+
279+ img . src = url ;
280+ } ) ;
281+ } ;
282+
283+ /**
284+ * Create a 128x128 preview data URL for an image file
285+ */
286+ export const createImagePreview = async ( file : File ) : Promise < string | null > => {
287+ if ( ! file . type . startsWith ( 'image/' ) || file . type === 'image/svg+xml' ) {
288+ return null ;
289+ }
290+
291+ const PREVIEW_SIZE = 128 ;
292+ const WEBP_QUALITY = 0.8 ;
293+
294+ return new Promise ( ( resolve ) => {
295+ const img = new Image ( ) ;
296+ const url = URL . createObjectURL ( file ) ;
297+
298+ img . onload = ( ) => {
299+ URL . revokeObjectURL ( url ) ;
300+
301+ let { width, height } = img ;
302+
303+ if ( width > height ) {
304+ height = Math . round ( ( height * PREVIEW_SIZE ) / width ) ;
305+ width = PREVIEW_SIZE ;
306+ } else {
307+ width = Math . round ( ( width * PREVIEW_SIZE ) / height ) ;
308+ height = PREVIEW_SIZE ;
309+ }
310+
311+ const canvas = document . createElement ( 'canvas' ) ;
312+ canvas . width = width ;
313+ canvas . height = height ;
314+ const ctx = canvas . getContext ( '2d' ) ;
315+ ctx ?. drawImage ( img , 0 , 0 , width , height ) ;
316+
317+ canvas . toBlob (
318+ ( blob ) => {
319+ if ( blob ) {
320+ const reader = new FileReader ( ) ;
321+ reader . onloadend = ( ) => {
322+ resolve ( reader . result as string ) ;
323+ } ;
324+ reader . readAsDataURL ( blob ) ;
325+ } else {
326+ resolve ( null ) ;
327+ }
328+ } ,
329+ 'image/webp' ,
330+ WEBP_QUALITY
331+ ) ;
332+ } ;
333+
334+ img . onerror = ( ) => {
335+ URL . revokeObjectURL ( url ) ;
336+ resolve ( null ) ;
337+ } ;
338+
339+ img . src = url ;
340+ } ) ;
187341} ;
0 commit comments