Skip to content

srcsetWidth methods return incorrect variants when retina sizes are configured #431

@lukastransom

Description

@lukastransom

Description

The srcsetWidth(), srcsetWidthWebP(), srcsetMinWidth(), and related methods return incorrect image variants when retina sizes (2x, 3x) are configured in an ImageOptimize field. This appears to be related to the issue addressed in PR #416, though that fix was apparently reverted due to regressions.

Steps to reproduce

  1. Create an ImageOptimize field with multiple variant widths (e.g., 559, 360, 442, 466, 573)
  2. Enable retina sizes (2x) for each variant
  3. In a template, call srcsetWidthWebP() with DPR mode:
    {% set optimizedImages = image.ioPostcard %}
    {{ optimizedImages.srcsetWidthWebP(559, true) }}

Expected behaviour

Should return the 559px variant and its 2x retina version:

.../559px/image.jpg.webp 1x, .../1118px/image.jpg.webp 2x

Actual Behavior

Returns completely unrelated variants:

.../884px/image.jpg.webp 1x, .../932px/image.jpg.webp 1x

Note: 884px and 932px are the 2x versions of other configured variants (442 and 466), not 559.

Source of the Issue

The getSrcsetSubsetArray() method in src/models/OptimizedImage.php uses array index positions to map variantSourceWidths to optimizedImageUrls. This breaks when:

  1. variantSourceWidths contains duplicates (one entry per retina variant)
  2. Both arrays are sorted independently
  3. array_slice($set, $index, 1, true) attempts to extract variants by index

After sorting, the indices no longer correctly map source widths to actual widths.

Example Data After Sorting

variantSourceWidths: ["360", "360", "442", "442", "466", "466", "559", "559", ...]
optimizedImageUrls keys: [360, 442, 466, 559, 573, 720, 884, 932, 1118, ...]

When searching for width 559:

  • Matches at indices 6 and 7 in variantSourceWidths
  • But array_slice() at index 6 grabs key 720 (wrong!)
  • And index 7 grabs key 884 (also wrong!)

Related Issues/PRs

  • PR Fix srcset width filtering #416 - Attempted to fix by removing the sort, but was reverted due to regressions
  • This affects all srcset*() methods when retina variants are enabled

Current Workaround

I'm currently using a Composer patch that fixes the getSrcsetSubsetArray() method by:

  1. Iterating through actual widths in the set
  2. Determining each width's source width by checking if it's a 1x, 2x, or 3x multiple of any configured variant
  3. Filtering based on the determined source width rather than array indices

This approach doesn't rely on index positions at all, so it works regardless of sorting and avoids the regressions from PR #416.

The patch is working perfectly on my production site. All srcset*() methods now return correct variants with proper 1x/2x descriptors.

Potential PR

I have a working fix that I'd be happy to submit as a PR if that would be helpful. The solution is, I feel, more robust than PR #416's approach and shouldn't cause regressions since it:

  • Doesn't remove sorting (avoiding PR Fix srcset width filtering #416's issues)
  • Uses explicit multiplier detection (1x, 2x, 3x)
  • Works with both sorted and unsorted data

Let me know if a PR would be welcome.

Patch file contents

--- a/src/models/OptimizedImage.php	2025-11-10 12:25:35
+++ b/src/models/OptimizedImage.php	2025-11-10 12:18:01
@@ -614,39 +614,54 @@
     protected function getSrcsetSubsetArray(array $set, int $width, string $comparison): array
     {
         $subset = [];
-        $index = 0;
         if (empty($this->variantSourceWidths)) {
             return $subset;
         }
-        // Sort the arrays by numeric key
-        ksort($set, SORT_NUMERIC);
-        // Sort the arrays by numeric key
-        sort($this->variantSourceWidths, SORT_NUMERIC);
-        foreach ($this->variantSourceWidths as $variantSourceWidth) {
+        
+        // For each actual width in the set, check if its source width matches
+        foreach ($set as $actualWidth => $url) {
+            // Find which variantSourceWidth this actualWidth belongs to
+            // Since retina variants are multiples (1x, 2x, 3x), we need to find the base width
+            $sourceWidth = null;
+            foreach (array_unique($this->variantSourceWidths) as $variantSourceWidth) {
+                // Check if actualWidth is 1x, 2x, or 3x of this variantSourceWidth
+                if ($actualWidth == $variantSourceWidth || 
+                    $actualWidth == $variantSourceWidth * 2 || 
+                    $actualWidth == $variantSourceWidth * 3) {
+                    $sourceWidth = $variantSourceWidth;
+                    break;
+                }
+            }
+            
+            if ($sourceWidth === null) {
+                continue;
+            }
+            
+            // Now check if this sourceWidth matches our comparison criteria
             $match = false;
             switch ($comparison) {
                 case 'width':
-                    if ($variantSourceWidth == $width) {
+                    if ($sourceWidth == $width) {
                         $match = true;
                     }
                     break;
 
                 case 'minwidth':
-                    if ($variantSourceWidth >= $width) {
+                    if ($sourceWidth >= $width) {
                         $match = true;
                     }
                     break;
 
                 case 'maxwidth':
-                    if ($variantSourceWidth <= $width) {
+                    if ($sourceWidth <= $width) {
                         $match = true;
                     }
                     break;
             }
+            
             if ($match) {
-                $subset += array_slice($set, $index, 1, true);
+                $subset[$actualWidth] = $url;
             }
-            $index++;
         }
 
         return $subset;

Versions

  • Craft CMS: 5.7+
  • ImageOptimize: 5.0.7
  • PHP: 8.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions