Skip to content

Commit a4cc39a

Browse files
committed
feat: enhance srcset detection to include crop status and improve logging for image processing
1 parent c37a3f9 commit a4cc39a

7 files changed

Lines changed: 318 additions & 62 deletions

File tree

assets/js/modules/README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ This directory contains the modular JavaScript components for the Optimole image
8989
- Includes images without `data-opt-src` (non-lazyload images)
9090
- Includes images with `data-opt-src` AND `data-opt-lazy-loaded` (completed lazyload)
9191
- Skips images with only `data-opt-src` (pending lazyload)
92+
- **Aspect ratio detection for cropping requirements**:
93+
- Compares natural vs. displayed aspect ratios to determine if cropping is needed
94+
- Uses intelligent thresholds: 5% tolerance, 15% significant difference, 10% ratio change
95+
- Prevents unnecessary cropping when aspect ratios match
96+
- Returns separate crop status data as `imageId: cropStatus` mapping
9297
- Calculates required srcset variations using comprehensive size ranges:
9398
- Mobile range: 200w-500w (50w steps) for dense mobile coverage
9499
- Tablet range: 500w-800w (100w steps) for tablet devices
@@ -101,6 +106,7 @@ This directory contains the modular JavaScript components for the Optimole image
101106
- `sizeTolerance`: Tolerance for existing sizes (default: 50px)
102107
- Analyzes existing srcset attributes to identify missing sizes
103108
- **Ultra-compact API payload**: Sends only essential fields with short names (w, h, d, s, b)
109+
- **Separate crop status**: Returns crop requirements as `imageId: cropStatus` mapping instead of per-srcset flags
104110
- **Full logging**: Complete analysis data available in console logs for debugging
105111
- Smart selection from dense size grid for optimal responsive coverage
106112

@@ -110,6 +116,47 @@ This directory contains the modular JavaScript components for the Optimole image
110116
- **Key Features**:
111117
- Complete above-the-fold detection workflow
112118
- Module coordination and data aggregation
113-
- API data preparation and submission
119+
- API data preparation and submission with separate crop status
114120
- Error handling and condition checking
121+
122+
## Data Structure
123+
124+
### API Payload Structure
125+
The main module sends data to the REST API with the following structure:
126+
127+
```javascript
128+
{
129+
d: deviceType, // Device type (1=mobile, 2=desktop)
130+
a: aboveTheFoldImages, // Array of above-fold image IDs
131+
b: backgroundSelectors, // Background image selectors
132+
u: url, // Page URL
133+
t: timestamp, // Request timestamp
134+
h: hmac, // Security hash
135+
l: lcpData, // LCP (Largest Contentful Paint) data
136+
m: missingDimensions, // Missing dimension data
137+
s: srcsetData, // Srcset variations data
138+
c: cropStatusData // Crop status mapping (imageId -> boolean)
139+
}
140+
```
141+
142+
### Srcset Data Structure
143+
Individual srcset entries contain:
144+
```javascript
145+
{
146+
w: width, // Image width
147+
h: height, // Image height
148+
d: dpr, // Device pixel ratio
149+
s: descriptor, // Srcset descriptor (e.g., "300w")
150+
b: breakpoint // CSS breakpoint
151+
}
152+
```
153+
154+
### Crop Status Data Structure
155+
Crop requirements are stored separately:
156+
```javascript
157+
{
158+
882936320: true, // Image ID -> requires cropping
159+
123456789: false // Image ID -> no cropping needed
160+
}
161+
```
115162

assets/js/modules/main.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,12 @@ export const optmlMain = {
112112
optmlLogger.info('Images with missing dimensions found:', Object.keys(imageDimensionsData).length);
113113

114114
// Detect images requiring srcset variations (non-lazyload images)
115-
const srcsetData = await optmlSrcsetDetector.detectMissingSrcsets();
115+
const srcsetResult = await optmlSrcsetDetector.detectMissingSrcsets();
116+
const srcsetData = srcsetResult.srcset;
117+
const cropStatusData = srcsetResult.crop;
116118

117119
optmlLogger.info('Images requiring srcset variations found:', Object.keys(srcsetData).length);
120+
optmlLogger.info('Images with crop status found:', Object.keys(cropStatusData).length);
118121

119122
// Process background image selectors if available
120123
let bgImageUrls = new Map();
@@ -177,7 +180,8 @@ export const optmlMain = {
177180
u: lcpData.bgUrls
178181
},
179182
m: imageDimensionsData, // m for missing dimensions
180-
s: srcsetData // s for srcset data
183+
s: srcsetData, // s for srcset data
184+
c: cropStatusData // c for crop status data
181185
};
182186

183187
optmlLogger.info('Sending data with LCP information:', {
@@ -188,6 +192,7 @@ export const optmlMain = {
188192
optmlLogger.info('Sending background selectors:', processedBgSelectors);
189193
optmlLogger.info('Sending dimension data for images:', imageDimensionsData);
190194
optmlLogger.info('Sending srcset data for images:', srcsetData);
195+
optmlLogger.info('Sending crop status data for images:', cropStatusData);
191196

192197
optmlApi.sendToRestApi(data);
193198
return data;

assets/js/modules/srcset-detector.js

Lines changed: 132 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

inc/rest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ public function optimizations( WP_REST_Request $request ) {
946946
}
947947
$bg_selectors = $request->get_param( 'b' );
948948
$lcp_data = $request->get_param( 'l' );
949+
$crop_status = $request->get_param( 'c' );
949950
$origin = $request->get_header( 'origin' );
950951
if ( empty( $origin ) || ! is_allowed_http_origin( $origin ) ) {
951952
return $this->response( 'Invalid origin', 'error' );
@@ -1007,6 +1008,12 @@ function ( $url ) {
10071008
$sanitized_missing_srcsets[ intval( $id ) ] = $srcset;
10081009
}
10091010
}
1011+
$sanitized_crop_status = [];
1012+
if ( ! empty( $crop_status ) ) {
1013+
foreach ( $crop_status as $id => $crop ) {
1014+
$sanitized_crop_status[ intval( $id ) ] = (bool) $crop;
1015+
}
1016+
}
10101017
$sanitized_lcp_data = [];
10111018
if ( ! empty( $lcp_data ) ) {
10121019
$sanitized_lcp_data['imageId'] = sanitize_text_field( $lcp_data['i'] ?? '' );
@@ -1025,10 +1032,10 @@ function ( $url ) {
10251032
}
10261033

10271034
if ( OPTML_DEBUG ) {
1028-
do_action( 'optml_log', 'Storing profile data: ' . $url . ' - ' . $device_type . ' - ' . print_r( $above_fold_images, true ) . print_r( $sanitized_selectors, true ) . print_r( $sanitized_lcp_data, true ) . print_r( $sanitized_missing_dimensions, true ) . print_r( $sanitized_missing_srcsets, true ) );
1035+
do_action( 'optml_log', 'Storing profile data: ' . $url . ' - ' . $device_type . ' - ' . print_r( $above_fold_images, true ) . print_r( $sanitized_selectors, true ) . print_r( $sanitized_lcp_data, true ) . print_r( $sanitized_missing_dimensions, true ) . print_r( $sanitized_missing_srcsets, true ) . print_r( $sanitized_crop_status, true ) );
10291036
}
10301037
$profile = new Profile();
1031-
$profile->store( $url, $device_type, $above_fold_images, $sanitized_selectors, $sanitized_lcp_data, $sanitized_missing_dimensions, $sanitized_missing_srcsets );
1038+
$profile->store( $url, $device_type, $above_fold_images, $sanitized_selectors, $sanitized_lcp_data, $sanitized_missing_dimensions, $sanitized_missing_srcsets, $sanitized_crop_status );
10321039
return $this->response( 'Above fold data stored successfully' );
10331040
}
10341041

0 commit comments

Comments
 (0)