• 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.3
/src/Twig/Components/Image.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\Twig\Components;
13

14
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15
use Symfony\UX\TwigComponent\Attribute\PostMount;
16
use Tito10047\ProgressiveImageBundle\Decorators\PathDecoratorInterface;
17
use Tito10047\ProgressiveImageBundle\DTO\BreakpointAssignment;
18
use Tito10047\ProgressiveImageBundle\DTO\ImageMetadata;
19
use Tito10047\ProgressiveImageBundle\Exception\PathResolutionException;
20
use Tito10047\ProgressiveImageBundle\ProgressiveImageBundle;
21
use Tito10047\ProgressiveImageBundle\Service\MetadataReaderInterface;
22
use Tito10047\ProgressiveImageBundle\Service\PreloadCollector;
23
use Tito10047\ProgressiveImageBundle\Service\ResponsiveAttributeGenerator;
24

25
#[AsTwigComponent]
26
final class Image
27
{
28
    public ?string $src = null;
29
    public ?string $filter = null;
30
    public ?string $alt = null;
31
    public ?string $pointInterest = null;
32
    /**
33
     * @var array<string, mixed>
34
     */
35
    public array $context = [];
36
        private ?ImageMetadata $metadata        = null;
37
    private string $decoratedSrc;
38
        private ?int           $decoratedWidth  = null;
39
        private ?int           $decoratedHeight = null;
40
    public bool $preload = false;
41
    public ?int $ttl = null;
42
    public string $priority = 'high';
43
    public ?string $sizes = null;
44
    public ?string $ratio = null;
45
        public ?bool $retina = null;
46
    /**
47
     * @var BreakpointAssignment[]
48
     */
49
    private array $breakpoinsts = [];
50
    /**
51
         * @var array{sizes: string, srcset: string, variables: array<string, string>}|null
52
     */
53
    private ?array $responsiveAttributes = null;
54

55
    /**
56
     * @param iterable<PathDecoratorInterface> $pathDecorator
57
     */
58
    public function __construct(
59
        private readonly MetadataReaderInterface $analyzer,
60
        private readonly iterable $pathDecorator,
61
        private readonly ?ResponsiveAttributeGenerator $responsiveAttributeGenerator,
62
        private readonly PreloadCollector $preloadCollector,
63
                private readonly string $framework = 'custom',
64
                private readonly bool $defaultRetina = true,
65
    ) {
66
    }
375✔
67

68
    #[PostMount]
69
    public function postMount(): void
70
    {
71
                if (null === $this->retina) {
375✔
72
                        $this->retina = $this->defaultRetina;
347✔
73
                }
74

75
                $this->decoratedSrc = $this->src;
375✔
76
                foreach ($this->pathDecorator as $decorator) {
375✔
77
                        $this->decoratedSrc = $decorator->decorate($this->decoratedSrc, $this->context);
57✔
78
                }
79

80
        try {
81
            $this->metadata = $this->analyzer->getMetadata($this->src);
375✔
82
        } catch (PathResolutionException) {
134✔
83
            $this->metadata = null;
134✔
84
        }
85
                $this->breakpoinsts = $this->sizes ? BreakpointAssignment::parseSegments($this->sizes, $this->ratio) : [];
375✔
86
                if ($this->breakpoinsts && $this->responsiveAttributeGenerator) {
375✔
87
                        $context = $this->context;
182✔
88
                        if ($this->filter) {
182✔
89
                                $context['filter'] = $this->filter;
×
90
                        }
NEW
91
                        $this->responsiveAttributes = $this->responsiveAttributeGenerator->generate($this->src, $this->breakpoinsts, $this->metadata->width ?? 0, $this->preload, $this->pointInterest, $context, $this->retina);
182✔
92
                } else {
93
                        $this->decoratedWidth  = $this->metadata?->width;
193✔
94
                        $this->decoratedHeight = $this->metadata?->height;
193✔
95
                        foreach ($this->pathDecorator as $decorator) {
193✔
96
                                $size = $decorator->getSize($this->decoratedSrc, $this->context);
43✔
97
                                if ($size) {
43✔
98
                                        $this->decoratedWidth  = $size['width'];
28✔
99
                                        $this->decoratedHeight = $size['height'];
28✔
100
                                }
101
                        }
102

103
                        if ($this->preload) {
193✔
104
                                $this->preloadCollector->add($this->decoratedSrc, 'image', $this->priority);
14✔
105
                        }
106
        }
107
    }
108

109
    public function getSrcset(): string
110
    {
111
        if (!$this->responsiveAttributes) {
301✔
112
            return '';
119✔
113
        }
114

115
        return "srcset=\"{$this->responsiveAttributes['srcset']}\"";
182✔
116
    }
117

118
    public function getResponsiveSizes(): string
119
    {
120
        if (!$this->responsiveAttributes) {
301✔
121
            return '';
119✔
122
        }
123

124
        return "sizes=\"{$this->responsiveAttributes['sizes']}\"";
182✔
125
    }
126

127
    public function getHash(): ?string
128
    {
129
        return $this->metadata?->originalHash;
346✔
130
    }
131

132
    public function getWidth(): ?int
133
    {
134
                return $this->decoratedWidth;
88✔
135
    }
136

137
    public function getHeight(): ?int
138
    {
139
                return $this->decoratedHeight;
178✔
140
        }
141

142
        /**
143
         * @return array<string, string>
144
         */
145
        public function getVariables(): array {
146
                return $this->responsiveAttributes['variables'] ?? [];
301✔
147
    }
148

149
    public function getDecoratedSrc(): string
150
    {
151
        return $this->decoratedSrc ?? $this->src;
345✔
152
    }
153

154
    public function getController(): string
155
    {
156
        return ProgressiveImageBundle::STIMULUS_CONTROLLER;
301✔
157
    }
158

159
        public function getFramework(): string {
160
                return $this->framework;
301✔
161
        }
162
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc