• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tito10047 / progressive-image-bundle / 21061797997

16 Jan 2026 09:22AM UTC coverage: 90.395% (+0.2%) from 90.185%
21061797997

push

github

web-flow
Merge pull request #1 from tito10047/rentina

Add retina image generation support

23 of 26 new or added lines in 4 files covered. (88.46%)

800 of 885 relevant lines covered (90.4%)

286.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.59
/src/Service/ResponsiveAttributeGenerator.php
1
<?php
2

3
/*
4
 * This file is part of the Progressive Image Bundle.
5
 *
6
 * (c) Jozef Môstka <https://github.com/tito10047/progressive-image-bundle>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace Tito10047\ProgressiveImageBundle\Service;
13

14
use Tito10047\ProgressiveImageBundle\DTO\BreakpointAssignment;
15
use Tito10047\ProgressiveImageBundle\UrlGenerator\ResponsiveImageUrlGeneratorInterface;
16

17
final class ResponsiveAttributeGenerator
18
{
19
    /**
20
     * @param array{
21
     *      layouts: array<string, array{
22
     *      min_viewport: int,
23
     *      max_container: int|null
24
     *      }>,
25
     *      columns: int
26
     *      } $gridConfig
27
     * @param array<string, string> $ratioConfig
28
         * @param int[] $retinaMultipliers
29
     */
30
    public function __construct(
31
        private array $gridConfig,
32
        private array $ratioConfig,
33
                private readonly array $retinaMultipliers,
34
        private readonly PreloadCollector $preloadCollector,
35
        private ResponsiveImageUrlGeneratorInterface $urlGenerator,
36
    ) {
37
    }
484✔
38

39
    /**
40
     * @param BreakpointAssignment[] $assignments
41
     *
42
         * @return array{sizes: string, srcset: string, variables: array<string, string>}
43
     */
44
        public function generate(string $path, array $assignments, int $originalWidth, bool $preload, ?string $pointInterest = null, array $context = [], bool $retina = false): array
45
    {
46
        $assignments = $this->sortAssignments($assignments);
330✔
47

48
        $sizesParts = [];
330✔
49
        $srcsetParts = [];
330✔
50
                $variables = [];
330✔
51
        $processedWidths = [];
330✔
52

53
        foreach ($assignments as $assignment) {
330✔
54
            $layout = $this->gridConfig['layouts'][$assignment->breakpoint] ?? null;
330✔
55
                        if (!$layout && 'default' === $assignment->breakpoint) {
330✔
56
                                foreach ($this->gridConfig['layouts'] as $l) {
58✔
57
                                        if (0 === $l['min_viewport']) {
58✔
58
                                                $layout = $l;
58✔
59
                                                break;
58✔
60
                                        }
61
                                }
62
                        }
63

64
            if (!$layout) {
330✔
65
                                throw new \InvalidArgumentException(sprintf('Breakpoint "%s" is not defined in the grid configuration.', $assignment->breakpoint));
×
66
            }
67

68
                        [$pixelWidth, $sizeValue, $cssValue] = $this->calculateDimensions($assignment, $layout);
330✔
69

70
            $size = $this->formatSizePart($layout['min_viewport'], $sizeValue);
330✔
71
            $sizesParts[] = $size;
330✔
72

73
                        $multipliers = $retina ? $this->retinaMultipliers : [1];
330✔
74

75
                        foreach ($multipliers as $multiplier) {
330✔
76
                                $mPixelWidth = (int) round($pixelWidth * $multiplier);
330✔
77
                                $url         = $this->generateUrl($path, $assignment, $mPixelWidth, $originalWidth, $processedWidths, $pointInterest, $context);
330✔
78

79
                                if ($url) {
330✔
80
                                        if ($preload && 1 === $multiplier) {
330✔
NEW
81
                                                $this->preloadCollector->add($url, 'image', 'high', "{$mPixelWidth}w", $size);
28✔
82
                                        }
83

84
                                        $srcsetParts[] = $url . " {$mPixelWidth}w";
330✔
85
                                }
86
            }
87

88
                        $ratio                              = $this->resolveRatio($assignment);
330✔
89
                        $suffix                             = 0 === $layout['min_viewport'] ? '' : '-' . $assignment->breakpoint;
330✔
90
                        $variables['--img-width' . $suffix] = $cssValue;
330✔
91
                        if ($ratio) {
330✔
92
                                $variables['--img-aspect' . $suffix] = (string) $ratio;
190✔
93
                        }
94
        }
95

96
        return [
330✔
97
            'sizes' => implode(', ', $sizesParts),
330✔
98
            'srcset' => implode(', ', $srcsetParts),
330✔
99
                        'variables' => $variables,
330✔
100
        ];
330✔
101
    }
102

103
    private function formatSizePart(int $minViewport, string $sizeValue): string
104
    {
105
        return $minViewport > 0
330✔
106
            ? "(min-width: {$minViewport}px) {$sizeValue}"
315✔
107
            : $sizeValue;
330✔
108
    }
109

110
    /**
111
     * @param BreakpointAssignment[] $assignments
112
     *
113
     * @return BreakpointAssignment[]
114
     */
115
    private function sortAssignments(array $assignments): array
116
    {
117
        usort($assignments, fn ($a, $b) => ($this->gridConfig['layouts'][$b->breakpoint]['min_viewport'] ?? 0) <=>
330✔
118
            ($this->gridConfig['layouts'][$a->breakpoint]['min_viewport'] ?? 0)
170✔
119
        );
330✔
120

121
        return $assignments;
330✔
122
    }
123

124
    /**
125
     * @param array{min_viewport: int, max_container: int|null} $layout
126
     *
127
         * @return array{0: float, 1: string, 2: string}
128
     */
129
    private function calculateDimensions(BreakpointAssignment $assignment, array $layout): array
130
    {
131
                if (null !== $assignment->widthPercent) {
330✔
132
                        $percentValue = (float) rtrim($assignment->widthPercent, '%');
44✔
133
                        $cssValue     = $assignment->widthPercent;
44✔
134

135
                        $maxContainer = $layout['max_container'];
44✔
136
                        if ($maxContainer) {
44✔
137
                                $pixelWidth = ($percentValue / 100) * $maxContainer;
44✔
138
                        } else {
139
                                $pixelWidth = ($percentValue / 100) * 1920;
29✔
140
                        }
141

142
                        return [$pixelWidth, round($pixelWidth) . 'px', $cssValue];
44✔
143
                }
144

145
                if (null !== $assignment->width) {
286✔
146
                        $pixelWidth = (float) $assignment->width;
15✔
147
                        $sizeValue  = $assignment->width . 'px';
15✔
148

149
                        return [$pixelWidth, $sizeValue, $sizeValue];
15✔
150
                }
151

152
        $totalCols = $this->gridConfig['columns'];
271✔
153
        $maxContainer = $layout['max_container'];
271✔
154

155
        if ($maxContainer) {
271✔
156
                        // Fixed container (e.g. 1320px) -> width in px
157
            $pixelWidth = ($assignment->columns / $totalCols) * $maxContainer;
256✔
158
            $sizeValue = round($pixelWidth).'px';
256✔
159
        } else {
160
                        // Fluid (null) -> width in vw
161
            $vwWidth = ($assignment->columns / $totalCols) * 100;
156✔
162
            $sizeValue = round($vwWidth).'vw';
156✔
163
                        // For URL calculation we estimate px width from some reasonable max-width (e.g. 1920)
164
            $pixelWidth = ($vwWidth / 100) * 1920;
156✔
165
        }
166

167
                return [$pixelWidth, $sizeValue, $sizeValue];
271✔
168
    }
169

170
    /**
171
     * @param array<int, bool> $processedWidths
172
     */
173
    private function generateUrl(
174
        string $path,
175
        BreakpointAssignment $assignment,
176
        int $basePixelWidth,
177
        int $originalWidth,
178
        array &$processedWidths,
179
        ?string $pointInterest = null,
180
                array $context = [],
181
        ): string {
182
        $ratio = $this->resolveRatio($assignment);
330✔
183

184
                $requestedWidth = $basePixelWidth;
330✔
185
                if ($originalWidth > 0 && $basePixelWidth > $originalWidth) {
330✔
186
                        $basePixelWidth = $originalWidth;
126✔
187
        }
188

189
        $targetH = $ratio ? (int) round($basePixelWidth / $ratio) : null;
330✔
190
                $url = $this->urlGenerator->generateUrl($path, $basePixelWidth, $targetH, $pointInterest, $context);
330✔
191

192
        return $url;
330✔
193
    }
194

195
    private function resolveRatio(BreakpointAssignment $assignment): ?float
196
    {
197
        $ratioString = $assignment->ratio ?? null;
330✔
198
        if (!$ratioString) {
330✔
199
            return null;
154✔
200
        }
201

202
                // If it's a key in ratioConfig, use that
203
        if (isset($this->ratioConfig[$ratioString])) {
190✔
204
                        $ratioString = $this->ratioConfig[$ratioString];
118✔
205
                }
206

207
                if (is_numeric($ratioString)) {
190✔
208
                        return (float) $ratioString;
133✔
209
        }
210

211
                // Otherwise try to parse format "16/9" or "3-4"
212
        if (preg_match('/^(\d+)[\/-](\d+)$/', $ratioString, $matches)) {
115✔
213
                        return (float) $matches[1] / (float) $matches[2];
100✔
214
                }
215

216
                // Or format "400x500"
217
                if (preg_match('/^(\d+)x(\d+)$/', $ratioString, $matches)) {
58✔
218
            return (float) $matches[1] / (float) $matches[2];
58✔
219
        }
220

221
                throw new \InvalidArgumentException(sprintf('Invalid ratio format or missing ratio configuration for: "%s"', $ratioString));
×
222
    }
223
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc