@@ -118,17 +118,18 @@ export const optmlSrcsetDetector = {
118118 /**
119119 * Detect all Optimole images that are NOT using lazyload (no data-opt-src)
120120 * and calculate missing srcset variations
121- * @returns {Promise<Object> } Promise that resolves to object mapping image IDs to their required srcset data
121+ * @returns {Promise<Object> } Promise that resolves to object with srcset data and crop status
122122 */
123123 detectMissingSrcsets : async function ( ) {
124124 const missingSrcsetData = { } ;
125+ const cropStatusData = { } ;
125126
126127 // Find all Optimole images
127128 const optimoleImages = document . querySelectorAll ( 'img[data-opt-id]' ) ;
128129
129130 if ( optimoleImages . length === 0 ) {
130131 optmlLogger . info ( 'No Optimole images found for srcset analysis' ) ;
131- return missingSrcsetData ;
132+ return { srcset : missingSrcsetData , crop : cropStatusData } ;
132133 }
133134
134135 optmlLogger . info ( `Found ${ optimoleImages . length } Optimole images, waiting for them to load...` ) ;
@@ -159,10 +160,11 @@ export const optmlSrcsetDetector = {
159160 optmlLogger . info ( `Image ${ imageId } ${ reason } , analyzing srcset requirements` ) ;
160161
161162 // Analyze the image and calculate required srcset variations
162- const missingSizes = this . _analyzeSrcsetRequirements ( img , imageId ) ;
163+ const analysisResult = this . _analyzeSrcsetRequirements ( img , imageId ) ;
163164
164- if ( missingSizes && missingSizes . length > 0 ) {
165- missingSrcsetData [ imageId ] = missingSizes ;
165+ if ( analysisResult && analysisResult . srcset && analysisResult . srcset . length > 0 ) {
166+ missingSrcsetData [ imageId ] = analysisResult . srcset ;
167+ cropStatusData [ imageId ] = analysisResult . requiresCropping ;
166168 }
167169 }
168170 } catch ( error ) {
@@ -171,7 +173,8 @@ export const optmlSrcsetDetector = {
171173 } ) ;
172174
173175 optmlLogger . info ( 'Images requiring srcset variations:' , Object . keys ( missingSrcsetData ) . length ) ;
174- return missingSrcsetData ;
176+ optmlLogger . info ( 'Images with crop status:' , Object . keys ( cropStatusData ) . length ) ;
177+ return { srcset : missingSrcsetData , crop : cropStatusData } ;
175178 } ,
176179
177180 /**
@@ -199,17 +202,24 @@ export const optmlSrcsetDetector = {
199202 natural : `${ naturalWidth } x${ naturalHeight } `
200203 } ) ;
201204
202- // Calculate aspect ratio
203- const aspectRatio = naturalWidth / naturalHeight ;
205+ // Calculate aspect ratios
206+ const naturalAspectRatio = naturalWidth / naturalHeight ;
207+ const currentAspectRatio = currentWidth / currentHeight ;
208+
209+ // Determine if image requires cropping based on aspect ratio difference
210+ const aspectRatioDifference = Math . abs ( naturalAspectRatio - currentAspectRatio ) ;
211+ const requiresCropping = this . _requiresCropping ( aspectRatioDifference , naturalAspectRatio , currentAspectRatio ) ;
204212
205213 // Get current device type
206214 const currentDeviceType = optmlDevice . getDeviceType ( ) ;
207215
208216 // Calculate required sizes for different breakpoints
217+ // Use current aspect ratio for srcset generation to match the rendered dimensions
218+ const aspectRatioForSizing = requiresCropping ? currentAspectRatio : naturalAspectRatio ;
209219 const requiredSizes = this . _calculateRequiredSizes (
210220 currentWidth ,
211221 currentHeight ,
212- aspectRatio ,
222+ aspectRatioForSizing ,
213223 currentDeviceType ,
214224 naturalWidth ,
215225 naturalHeight
@@ -231,20 +241,68 @@ export const optmlSrcsetDetector = {
231241 optmlLogger . info ( `Image ${ imageId } srcset analysis:` , {
232242 currentSize : { w : currentWidth , h : currentHeight } ,
233243 naturalSize : { w : naturalWidth , h : naturalHeight } ,
234- aspectRatio : Math . round ( aspectRatio * 1000 ) / 1000 ,
244+ naturalAspectRatio : Math . round ( naturalAspectRatio * 1000 ) / 1000 ,
245+ currentAspectRatio : Math . round ( currentAspectRatio * 1000 ) / 1000 ,
246+ aspectRatioDifference : Math . round ( aspectRatioDifference * 1000 ) / 1000 ,
247+ requiresCropping : requiresCropping ,
248+ aspectRatioForSizing : Math . round ( aspectRatioForSizing * 1000 ) / 1000 ,
235249 deviceType : currentDeviceType ,
236250 missingSizes : missingSizes ,
237251 existingSrcset : existingSrcset || null
238252 } ) ;
253+
254+ // Additional debug logging for aspect ratio analysis
255+ console . log ( `[Optimole Debug] Image ${ imageId } aspect ratio analysis:` , {
256+ natural : `${ naturalWidth } x${ naturalHeight } (${ Math . round ( naturalAspectRatio * 1000 ) / 1000 } :1)` ,
257+ current : `${ currentWidth } x${ currentHeight } (${ Math . round ( currentAspectRatio * 1000 ) / 1000 } :1)` ,
258+ difference : Math . round ( aspectRatioDifference * 1000 ) / 1000 ,
259+ requiresCropping : requiresCropping ,
260+ aspectRatioForSizing : Math . round ( aspectRatioForSizing * 1000 ) / 1000 ,
261+ reason : requiresCropping ? 'Aspect ratio significantly different' : 'Aspect ratios match within tolerance'
262+ } ) ;
263+
264+ // Return both srcset data and crop status separately
265+ return {
266+ srcset : missingSizes . map ( size => ( {
267+ w : size . w ,
268+ h : size . h ,
269+ d : size . dpr , // dpr -> d
270+ s : size . descriptor , // descriptor -> s (srcset)
271+ b : size . breakpoint // breakpoint -> b
272+ } ) ) ,
273+ requiresCropping : requiresCropping
274+ } ;
275+ } ,
276+
277+ /**
278+ * Determine if an image requires cropping based on aspect ratio differences
279+ * @private
280+ * @param {number } aspectRatioDifference - Absolute difference between natural and current aspect ratios
281+ * @param {number } naturalAspectRatio - Natural image aspect ratio
282+ * @param {number } currentAspectRatio - Current displayed aspect ratio
283+ * @returns {boolean } True if the image requires cropping
284+ */
285+ _requiresCropping : function ( aspectRatioDifference , naturalAspectRatio , currentAspectRatio ) {
286+ // Define thresholds for determining when cropping is needed
287+ const ASPECT_RATIO_TOLERANCE = 0.05 ; // 5% tolerance for minor differences
288+ const SIGNIFICANT_DIFFERENCE_THRESHOLD = 0.15 ; // 15% for significant differences
289+
290+ // If the difference is very small, no cropping needed
291+ if ( aspectRatioDifference <= ASPECT_RATIO_TOLERANCE ) {
292+ return false ;
293+ }
294+
295+ // If the difference is significant, definitely needs cropping
296+ if ( aspectRatioDifference >= SIGNIFICANT_DIFFERENCE_THRESHOLD ) {
297+ return true ;
298+ }
299+
300+ // For moderate differences, check if the current aspect ratio is significantly different
301+ // from the natural one (indicating intentional resizing that would require cropping)
302+ const ratioChange = Math . abs ( currentAspectRatio - naturalAspectRatio ) / naturalAspectRatio ;
239303
240- // Return only essential fields for API (ultra-minimal payload with short names)
241- return missingSizes . map ( size => ( {
242- w : size . w ,
243- h : size . h ,
244- d : size . dpr , // dpr -> d
245- s : size . descriptor , // descriptor -> s (srcset)
246- b : size . breakpoint // breakpoint -> b
247- } ) ) ;
304+ // If the current aspect ratio is more than 10% different from natural, likely needs cropping
305+ return ratioChange > 0.1 ;
248306 } ,
249307
250308 /**
@@ -329,23 +387,45 @@ export const optmlSrcsetDetector = {
329387 _generateResponsiveSizes : function ( currentWidth , currentHeight , aspectRatio , naturalWidth , naturalHeight ) {
330388 const sizes = [ ] ;
331389
332- // Generate comprehensive responsive sizes with better coverage
333- // Strategy: Create a dense grid of sizes for better responsive coverage
390+ // Container-based size generation: Generate sizes relative to the actual container size
391+ // This prevents oversized images from being selected for small containers
334392
335- // Define size ranges for different device categories
336- const sizeRanges = [
337- // Mobile range: 200w - 500w (step 50w) - dense coverage for mobile
338- { min : 200 , max : 500 , step : 50 , dpr : [ 1 ] , category : 'mobile' } ,
339-
340- // Tablet range: 500w - 800w (step 100w) - good tablet coverage
341- { min : 500 , max : 800 , step : 100 , dpr : [ 1 ] , category : 'tablet' } ,
342-
343- // Desktop range: 800w - 1200w (step 200w) - desktop coverage
344- { min : 800 , max : 1200 , step : 200 , dpr : [ 1 ] , category : 'desktop' } ,
345-
346- // High-res range: 1200w - 1600w (step 200w) with selective 2x - practical high-res
347- { min : 1200 , max : 1600 , step : 200 , dpr : [ 1 , 2 ] , category : 'high-res' }
348- ] ;
393+ // Calculate maximum reasonable size for this container
394+ // Use a multiplier to allow for some flexibility while staying close to container size
395+ const maxSizeMultiplier = 1.5 ; // Allow up to 1.5x the container size
396+ const maxReasonableWidth = Math . min (
397+ Math . round ( currentWidth * maxSizeMultiplier ) ,
398+ naturalWidth
399+ ) ;
400+
401+ // Define size ranges based on container size
402+ let sizeRanges = [ ] ;
403+
404+ if ( currentWidth <= 300 ) {
405+ // Small containers (thumbnails, icons, etc.)
406+ sizeRanges = [
407+ { min : Math . max ( 200 , Math . round ( currentWidth * 0.8 ) ) , max : Math . round ( currentWidth * 1.2 ) , step : 25 , dpr : [ 1 ] , category : 'small' } ,
408+ { min : Math . round ( currentWidth * 1.2 ) , max : maxReasonableWidth , step : 50 , dpr : [ 1 , 2 ] , category : 'small-retina' }
409+ ] ;
410+ } else if ( currentWidth <= 600 ) {
411+ // Medium containers (cards, medium images)
412+ sizeRanges = [
413+ { min : Math . max ( 200 , Math . round ( currentWidth * 0.8 ) ) , max : Math . round ( currentWidth * 1.2 ) , step : 50 , dpr : [ 1 ] , category : 'medium' } ,
414+ { min : Math . round ( currentWidth * 1.2 ) , max : maxReasonableWidth , step : 100 , dpr : [ 1 , 2 ] , category : 'medium-retina' }
415+ ] ;
416+ } else if ( currentWidth <= 1000 ) {
417+ // Large containers (featured images, banners)
418+ sizeRanges = [
419+ { min : Math . max ( 200 , Math . round ( currentWidth * 0.8 ) ) , max : Math . round ( currentWidth * 1.2 ) , step : 100 , dpr : [ 1 ] , category : 'large' } ,
420+ { min : Math . round ( currentWidth * 1.2 ) , max : maxReasonableWidth , step : 200 , dpr : [ 1 , 2 ] , category : 'large-retina' }
421+ ] ;
422+ } else {
423+ // Very large containers (full-width images)
424+ sizeRanges = [
425+ { min : Math . max ( 200 , Math . round ( currentWidth * 0.8 ) ) , max : Math . round ( currentWidth * 1.2 ) , step : 200 , dpr : [ 1 ] , category : 'xlarge' } ,
426+ { min : Math . round ( currentWidth * 1.2 ) , max : maxReasonableWidth , step : 400 , dpr : [ 1 , 2 ] , category : 'xlarge-retina' }
427+ ] ;
428+ }
349429
350430 // Generate sizes for each range
351431 sizeRanges . forEach ( range => {
@@ -358,13 +438,25 @@ export const optmlSrcsetDetector = {
358438 if ( this . _isValidSize ( targetWidth , targetHeight , naturalWidth , naturalHeight ) &&
359439 targetWidth >= this . CONFIG . MIN_SIZE ) {
360440
361- // Determine breakpoint based on width
441+ // Determine breakpoint based on container size, not image size
442+ // Use viewport-based breakpoints that make sense for the container
362443 let breakpoint = null ;
363- if ( targetWidth <= 400 ) breakpoint = 320 ;
364- else if ( targetWidth <= 600 ) breakpoint = 768 ;
365- else if ( targetWidth <= 900 ) breakpoint = 1024 ;
366- else if ( targetWidth <= 1200 ) breakpoint = 1440 ;
367- else breakpoint = 1920 ;
444+ if ( currentWidth <= 300 ) {
445+ // Small containers: use smaller breakpoints
446+ if ( targetWidth <= currentWidth * 1.1 ) breakpoint = 480 ;
447+ else if ( targetWidth <= currentWidth * 1.3 ) breakpoint = 768 ;
448+ else breakpoint = 1024 ;
449+ } else if ( currentWidth <= 600 ) {
450+ // Medium containers: use medium breakpoints
451+ if ( targetWidth <= currentWidth * 1.1 ) breakpoint = 768 ;
452+ else if ( targetWidth <= currentWidth * 1.3 ) breakpoint = 1024 ;
453+ else breakpoint = 1440 ;
454+ } else {
455+ // Large containers: use larger breakpoints
456+ if ( targetWidth <= currentWidth * 1.1 ) breakpoint = 1024 ;
457+ else if ( targetWidth <= currentWidth * 1.3 ) breakpoint = 1440 ;
458+ else breakpoint = 1920 ;
459+ }
368460
369461 sizes . push ( {
370462 w : targetWidth ,
0 commit comments