xref: /plugin/combo/ComboStrap/FetcherImage.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau
3*04fd306cSNickeaunamespace ComboStrap;
4*04fd306cSNickeau
5*04fd306cSNickeauuse ComboStrap\Web\Url;
6*04fd306cSNickeau
7*04fd306cSNickeau/**
8*04fd306cSNickeau * Image request / response
9*04fd306cSNickeau *
10*04fd306cSNickeau * with requested attribute (ie a file and its transformation attribute if any such as
11*04fd306cSNickeau * width, height, ...)
12*04fd306cSNickeau *
13*04fd306cSNickeau * Image may be generated that's why they don't extends {@link FetcherRawLocalPath}.
14*04fd306cSNickeau * Image that depends on a source file use the {@link FetcherTraitWikiPath} and extends {@link IFetcherLocalImage}
15*04fd306cSNickeau *
16*04fd306cSNickeau * See also third provider such as:
17*04fd306cSNickeau *   * https://docs.imgix.com/setup/quick-start - still need to host them (https://docs.imgix.com/apis/rendering)
18*04fd306cSNickeau *
19*04fd306cSNickeau *
20*04fd306cSNickeau *
21*04fd306cSNickeau */
22*04fd306cSNickeauabstract class FetcherImage extends IFetcherAbs implements IFetcherPath
23*04fd306cSNickeau{
24*04fd306cSNickeau
25*04fd306cSNickeau    const TOK = "tok";
26*04fd306cSNickeau    const CANONICAL = "image";
27*04fd306cSNickeau
28*04fd306cSNickeau
29*04fd306cSNickeau    protected ?int $requestedWidth = null;
30*04fd306cSNickeau    protected ?int $requestedHeight = null;
31*04fd306cSNickeau
32*04fd306cSNickeau    private ?string $requestedRatio = null;
33*04fd306cSNickeau    private ?float $requestedRatioAsFloat = null;
34*04fd306cSNickeau
35*04fd306cSNickeau
36*04fd306cSNickeau    /**
37*04fd306cSNickeau     * Image Fetch constructor.
38*04fd306cSNickeau     *
39*04fd306cSNickeau     */
40*04fd306cSNickeau    public function __construct()
41*04fd306cSNickeau    {
42*04fd306cSNickeau        /**
43*04fd306cSNickeau         * Image can be generated, ie {@link FetcherVignette}, {@link FetcherScreenshot}
44*04fd306cSNickeau         */
45*04fd306cSNickeau    }
46*04fd306cSNickeau
47*04fd306cSNickeau
48*04fd306cSNickeau    /**
49*04fd306cSNickeau     * @param Url|null $url
50*04fd306cSNickeau     *
51*04fd306cSNickeau     */
52*04fd306cSNickeau    public function getFetchUrl(Url $url = null): Url
53*04fd306cSNickeau    {
54*04fd306cSNickeau        $url = parent::getFetchUrl($url);
55*04fd306cSNickeau
56*04fd306cSNickeau        try {
57*04fd306cSNickeau            $ratio = $this->getRequestedAspectRatio();
58*04fd306cSNickeau            $url->addQueryParameterIfNotPresent(Dimension::RATIO_ATTRIBUTE, $ratio);
59*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
60*04fd306cSNickeau            // no width ok
61*04fd306cSNickeau        }
62*04fd306cSNickeau
63*04fd306cSNickeau        try {
64*04fd306cSNickeau            $requestedWidth = $this->getRequestedWidth();
65*04fd306cSNickeau            $url->addQueryParameterIfNotPresent(Dimension::WIDTH_KEY_SHORT, $requestedWidth);
66*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
67*04fd306cSNickeau            // no width ok
68*04fd306cSNickeau        }
69*04fd306cSNickeau
70*04fd306cSNickeau        try {
71*04fd306cSNickeau            $requestedHeight = $this->getRequestedHeight();
72*04fd306cSNickeau            $url->addQueryParameterIfNotPresent(Dimension::HEIGHT_KEY_SHORT, $requestedHeight);
73*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
74*04fd306cSNickeau            // no height ok
75*04fd306cSNickeau        }
76*04fd306cSNickeau
77*04fd306cSNickeau
78*04fd306cSNickeau        /**
79*04fd306cSNickeau         * Dokuwiki Conformance
80*04fd306cSNickeau         */
81*04fd306cSNickeau        try {
82*04fd306cSNickeau            $url->addQueryParameter(FetcherImage::TOK, $this->getTok());
83*04fd306cSNickeau        } catch (ExceptionNotNeeded $e) {
84*04fd306cSNickeau            // ok not needed
85*04fd306cSNickeau        }
86*04fd306cSNickeau
87*04fd306cSNickeau
88*04fd306cSNickeau        return $url;
89*04fd306cSNickeau    }
90*04fd306cSNickeau
91*04fd306cSNickeau    /**
92*04fd306cSNickeau     * The tok is supposed to counter a DDOS attack when
93*04fd306cSNickeau     * with or height are requested
94*04fd306cSNickeau     *
95*04fd306cSNickeau     *
96*04fd306cSNickeau     * @throws ExceptionNotNeeded
97*04fd306cSNickeau     */
98*04fd306cSNickeau    public function getTok(): string
99*04fd306cSNickeau    {
100*04fd306cSNickeau        /**
101*04fd306cSNickeau         * Dokuwiki Compliance
102*04fd306cSNickeau         */
103*04fd306cSNickeau        if (!($this instanceof IFetcherLocalImage)) {
104*04fd306cSNickeau            throw new ExceptionNotNeeded("No tok for non local image");
105*04fd306cSNickeau        }
106*04fd306cSNickeau        try {
107*04fd306cSNickeau            $requestedWidth = $this->getRequestedWidth();
108*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
109*04fd306cSNickeau            $requestedWidth = null;
110*04fd306cSNickeau        }
111*04fd306cSNickeau        try {
112*04fd306cSNickeau            $requestedHeight = $this->getRequestedHeight();
113*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
114*04fd306cSNickeau            $requestedHeight = null;
115*04fd306cSNickeau        }
116*04fd306cSNickeau        if ($requestedWidth !== null || $requestedHeight !== null) {
117*04fd306cSNickeau
118*04fd306cSNickeau            try {
119*04fd306cSNickeau                $id = $this->getSourcePath()->toWikiPath()->getWikiId();
120*04fd306cSNickeau            } catch (ExceptionCast $e) {
121*04fd306cSNickeau                LogUtility::error("Unable to calculate the image tok. The source path is not a web/wiki path", self::CANONICAL, $e);
122*04fd306cSNickeau                throw new ExceptionNotNeeded("No tok added, error " . $e->getMessage());
123*04fd306cSNickeau            }
124*04fd306cSNickeau            return media_get_token($id, $requestedWidth, $requestedHeight);
125*04fd306cSNickeau
126*04fd306cSNickeau        }
127*04fd306cSNickeau        throw new ExceptionNotNeeded("No tok needed");
128*04fd306cSNickeau    }
129*04fd306cSNickeau
130*04fd306cSNickeau    /**
131*04fd306cSNickeau     * @throws ExceptionBadArgument
132*04fd306cSNickeau     */
133*04fd306cSNickeau    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherImage
134*04fd306cSNickeau    {
135*04fd306cSNickeau
136*04fd306cSNickeau        $requestedWidth = $tagAttributes->getValueAndRemove(Dimension::WIDTH_KEY);
137*04fd306cSNickeau        if ($requestedWidth === null) {
138*04fd306cSNickeau            $requestedWidth = $tagAttributes->getValueAndRemove(Dimension::WIDTH_KEY_SHORT);
139*04fd306cSNickeau        }
140*04fd306cSNickeau        if ($requestedWidth !== null) {
141*04fd306cSNickeau            try {
142*04fd306cSNickeau                $requestedWidthInt = DataType::toInteger(ConditionalLength::createFromString($requestedWidth)->toPixelNumber());
143*04fd306cSNickeau            } catch (ExceptionBadArgument $e) {
144*04fd306cSNickeau                throw new ExceptionBadArgument("The width value ($requestedWidth) is not a valid integer", FetcherImage::CANONICAL, 0, $e);
145*04fd306cSNickeau            }
146*04fd306cSNickeau            $this->setRequestedWidth($requestedWidthInt);
147*04fd306cSNickeau        }
148*04fd306cSNickeau
149*04fd306cSNickeau        $requestedHeight = $tagAttributes->getValueAndRemove(Dimension::HEIGHT_KEY);
150*04fd306cSNickeau        if ($requestedHeight === null) {
151*04fd306cSNickeau            $requestedHeight = $tagAttributes->getValueAndRemove(Dimension::HEIGHT_KEY_SHORT);
152*04fd306cSNickeau        }
153*04fd306cSNickeau        if ($requestedHeight !== null) {
154*04fd306cSNickeau            try {
155*04fd306cSNickeau                $requestedHeightInt = DataType::toInteger($requestedHeight);
156*04fd306cSNickeau            } catch (ExceptionBadArgument $e) {
157*04fd306cSNickeau                throw new ExceptionBadArgument("The height value ($requestedHeight) is not a valid integer", FetcherImage::CANONICAL, 0, $e);
158*04fd306cSNickeau            }
159*04fd306cSNickeau            $this->setRequestedHeight($requestedHeightInt);
160*04fd306cSNickeau        }
161*04fd306cSNickeau
162*04fd306cSNickeau        $requestedRatio = $tagAttributes->getValueAndRemove(Dimension::RATIO_ATTRIBUTE);
163*04fd306cSNickeau        if ($requestedRatio !== null) {
164*04fd306cSNickeau            try {
165*04fd306cSNickeau                $this->setRequestedAspectRatio($requestedRatio);
166*04fd306cSNickeau            } catch (ExceptionBadSyntax $e) {
167*04fd306cSNickeau                throw new ExceptionBadArgument("The requested ratio ($requestedRatio) is not a valid value ({$e->getMessage()})", FetcherImage::CANONICAL, 0, $e);
168*04fd306cSNickeau            }
169*04fd306cSNickeau        }
170*04fd306cSNickeau        parent::buildFromTagAttributes($tagAttributes);
171*04fd306cSNickeau        return $this;
172*04fd306cSNickeau    }
173*04fd306cSNickeau
174*04fd306cSNickeau
175*04fd306cSNickeau    /**
176*04fd306cSNickeau     * For a raster image, the internal width
177*04fd306cSNickeau     * for a svg, the defined viewBox
178*04fd306cSNickeau     *
179*04fd306cSNickeau     * @return int in pixel
180*04fd306cSNickeau     */
181*04fd306cSNickeau    public
182*04fd306cSNickeau
183*04fd306cSNickeau    abstract function getIntrinsicWidth(): int;
184*04fd306cSNickeau
185*04fd306cSNickeau    /**
186*04fd306cSNickeau     * For a raster image, the internal height
187*04fd306cSNickeau     * for a svg, the defined `viewBox` value
188*04fd306cSNickeau     *
189*04fd306cSNickeau     * This is needed to calculate the {@link MediaLink::getTargetRatio() target ratio}
190*04fd306cSNickeau     * and pass them to the img tag to avoid layout shift
191*04fd306cSNickeau     *
192*04fd306cSNickeau     * @return int in pixel
193*04fd306cSNickeau     */
194*04fd306cSNickeau    public abstract function getIntrinsicHeight(): int;
195*04fd306cSNickeau
196*04fd306cSNickeau    /**
197*04fd306cSNickeau     * The Aspect ratio as explained here
198*04fd306cSNickeau     * https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
199*04fd306cSNickeau     * @return float
200*04fd306cSNickeau     * false if the image is not supported
201*04fd306cSNickeau     *
202*04fd306cSNickeau     * It's needed for an img tag to set the img `width` and `height` that pass the
203*04fd306cSNickeau     * {@link MediaLink::checkWidthAndHeightRatioAndReturnTheGoodValue() check}
204*04fd306cSNickeau     * to avoid layout shift
205*04fd306cSNickeau     *
206*04fd306cSNickeau     */
207*04fd306cSNickeau    public function getIntrinsicAspectRatio(): float
208*04fd306cSNickeau    {
209*04fd306cSNickeau
210*04fd306cSNickeau        return $this->getIntrinsicWidth() / $this->getIntrinsicHeight();
211*04fd306cSNickeau
212*04fd306cSNickeau    }
213*04fd306cSNickeau
214*04fd306cSNickeau    /**
215*04fd306cSNickeau     * The Aspect ratio of the target image (may be the original or the an image scaled down)
216*04fd306cSNickeau     *
217*04fd306cSNickeau     * https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
218*04fd306cSNickeau     * @return float
219*04fd306cSNickeau     * false if the image is not supported
220*04fd306cSNickeau     *
221*04fd306cSNickeau     * It's needed for an img tag to set the img `width` and `height` that pass the
222*04fd306cSNickeau     * {@link MediaLink::checkWidthAndHeightRatioAndReturnTheGoodValue() check}
223*04fd306cSNickeau     * to avoid layout shift
224*04fd306cSNickeau     *
225*04fd306cSNickeau     */
226*04fd306cSNickeau    public function getTargetAspectRatio()
227*04fd306cSNickeau    {
228*04fd306cSNickeau
229*04fd306cSNickeau        return $this->getTargetWidth() / $this->getTargetHeight();
230*04fd306cSNickeau
231*04fd306cSNickeau    }
232*04fd306cSNickeau
233*04fd306cSNickeau    /**
234*04fd306cSNickeau     * Return the requested aspect ratio requested
235*04fd306cSNickeau     * with the property
236*04fd306cSNickeau     * or if the width and height were specified.
237*04fd306cSNickeau     *
238*04fd306cSNickeau     * The Aspect ratio as explained here
239*04fd306cSNickeau     * https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
240*04fd306cSNickeau     * @return float
241*04fd306cSNickeau     *
242*04fd306cSNickeau     *
243*04fd306cSNickeau     * It's needed for an img tag to set the img `width` and `height` that pass the
244*04fd306cSNickeau     * {@link MediaLink::checkWidthAndHeightRatioAndReturnTheGoodValue() check}
245*04fd306cSNickeau     * to avoid layout shift
246*04fd306cSNickeau     * @throws ExceptionNotFound
247*04fd306cSNickeau     */
248*04fd306cSNickeau    public function getCalculatedRequestedAspectRatioAsFloat(): float
249*04fd306cSNickeau    {
250*04fd306cSNickeau
251*04fd306cSNickeau        if ($this->requestedRatioAsFloat !== null) {
252*04fd306cSNickeau            return $this->requestedRatioAsFloat;
253*04fd306cSNickeau        }
254*04fd306cSNickeau
255*04fd306cSNickeau        /**
256*04fd306cSNickeau         * Note: requested weight and width throw a `not found` if width / height == 0
257*04fd306cSNickeau         * No division by zero then
258*04fd306cSNickeau         */
259*04fd306cSNickeau        return $this->getRequestedWidth() / $this->getRequestedHeight();
260*04fd306cSNickeau
261*04fd306cSNickeau
262*04fd306cSNickeau    }
263*04fd306cSNickeau
264*04fd306cSNickeau
265*04fd306cSNickeau    /**
266*04fd306cSNickeau     * Giving width and height, check that the aspect ratio is the same
267*04fd306cSNickeau     * than the target one
268*04fd306cSNickeau     * @param $height
269*04fd306cSNickeau     * @param $width
270*04fd306cSNickeau     */
271*04fd306cSNickeau    public
272*04fd306cSNickeau    function checkLogicalRatioAgainstTargetRatio($width, $height)
273*04fd306cSNickeau    {
274*04fd306cSNickeau        /**
275*04fd306cSNickeau         * Check of height and width dimension
276*04fd306cSNickeau         * as specified here
277*04fd306cSNickeau         *
278*04fd306cSNickeau         * This is about the intrinsic dimension but we have the notion of target dimension
279*04fd306cSNickeau         *
280*04fd306cSNickeau         * https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
281*04fd306cSNickeau         */
282*04fd306cSNickeau        try {
283*04fd306cSNickeau            $targetRatio = $this->getTargetAspectRatio();
284*04fd306cSNickeau        } catch (ExceptionCompile $e) {
285*04fd306cSNickeau            LogUtility::msg("Unable to check the target ratio because it returns this error: {$e->getMessage()}");
286*04fd306cSNickeau            return;
287*04fd306cSNickeau        }
288*04fd306cSNickeau        if (!(
289*04fd306cSNickeau            $height * $targetRatio >= $width - 1
290*04fd306cSNickeau            &&
291*04fd306cSNickeau            $height * $targetRatio <= $width + 1
292*04fd306cSNickeau        )) {
293*04fd306cSNickeau            // check the second statement
294*04fd306cSNickeau            if (!(
295*04fd306cSNickeau                $width / $targetRatio >= $height - 1
296*04fd306cSNickeau                &&
297*04fd306cSNickeau                $width / $targetRatio <= $height + 1
298*04fd306cSNickeau            )) {
299*04fd306cSNickeau
300*04fd306cSNickeau                /**
301*04fd306cSNickeau                 * Programmatic error from the developer
302*04fd306cSNickeau                 */
303*04fd306cSNickeau                $imgTagRatio = $width / $height;
304*04fd306cSNickeau                LogUtility::msg("Internal Error: The width ($width) and height ($height) calculated for the image ($this) does not pass the ratio test. They have a ratio of ($imgTagRatio) while the target dimension ratio is ($targetRatio)");
305*04fd306cSNickeau
306*04fd306cSNickeau            }
307*04fd306cSNickeau        }
308*04fd306cSNickeau    }
309*04fd306cSNickeau
310*04fd306cSNickeau
311*04fd306cSNickeau    /**
312*04fd306cSNickeau     * The logical height is the calculated height of the target image
313*04fd306cSNickeau     * specified in the query parameters
314*04fd306cSNickeau     *
315*04fd306cSNickeau     * For instance,
316*04fd306cSNickeau     *   * with `200`, the target image has a {@link FetcherTraitImage::getTargetWidth() logical width} of 200 and a {@link FetcherTraitImage::getTargetHeight() logical height} that is scaled down by the {@link FetcherTraitImage::getIntrinsicAspectRatio() instrinsic ratio}
317*04fd306cSNickeau     *   * with ''0x20'', the target image has a {@link FetcherTraitImage::getTargetHeight() logical height} of 20 and a {@link FetcherTraitImage::getTargetWidth() logical width} that is scaled down by the {@link FetcherTraitImage::getIntrinsicAspectRatio() instrinsic ratio}
318*04fd306cSNickeau     *
319*04fd306cSNickeau     * The doc is {@link https://www.dokuwiki.org/images#resizing}
320*04fd306cSNickeau     *
321*04fd306cSNickeau     *
322*04fd306cSNickeau     * @return int
323*04fd306cSNickeau     */
324*04fd306cSNickeau    public function getTargetHeight(): int
325*04fd306cSNickeau    {
326*04fd306cSNickeau
327*04fd306cSNickeau        try {
328*04fd306cSNickeau            return $this->getRequestedHeight();
329*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
330*04fd306cSNickeau            // no height
331*04fd306cSNickeau        }
332*04fd306cSNickeau
333*04fd306cSNickeau        /**
334*04fd306cSNickeau         * Scaled down by width
335*04fd306cSNickeau         */
336*04fd306cSNickeau        try {
337*04fd306cSNickeau            $width = $this->getRequestedWidth();
338*04fd306cSNickeau            try {
339*04fd306cSNickeau                $ratio = $this->getCalculatedRequestedAspectRatioAsFloat();
340*04fd306cSNickeau            } catch (ExceptionNotFound $e) {
341*04fd306cSNickeau                $ratio = $this->getIntrinsicAspectRatio();
342*04fd306cSNickeau            }
343*04fd306cSNickeau            return self::round($width / $ratio);
344*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
345*04fd306cSNickeau            // no width
346*04fd306cSNickeau        }
347*04fd306cSNickeau
348*04fd306cSNickeau
349*04fd306cSNickeau        /**
350*04fd306cSNickeau         * Scaled down by ratio
351*04fd306cSNickeau         */
352*04fd306cSNickeau        try {
353*04fd306cSNickeau            $ratio = $this->getCalculatedRequestedAspectRatioAsFloat();
354*04fd306cSNickeau            [$croppedWidth, $croppedHeight] = $this->getCroppingDimensionsWithRatio($ratio);
355*04fd306cSNickeau            return $croppedHeight;
356*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
357*04fd306cSNickeau            // no requested aspect ratio
358*04fd306cSNickeau        }
359*04fd306cSNickeau
360*04fd306cSNickeau        return $this->getIntrinsicHeight();
361*04fd306cSNickeau
362*04fd306cSNickeau    }
363*04fd306cSNickeau
364*04fd306cSNickeau    /**
365*04fd306cSNickeau     * The logical width is the width of the target image calculated from the requested dimension
366*04fd306cSNickeau     *
367*04fd306cSNickeau     * For instance,
368*04fd306cSNickeau     *   * with `200`, the target image has a {@link FetcherTraitImage::getTargetWidth() logical width} of 200 and a {@link FetcherTraitImage::getTargetHeight() logical height} that is scaled down by the {@link FetcherTraitImage::getIntrinsicAspectRatio() instrinsic ratio}
369*04fd306cSNickeau     *   * with ''0x20'', the target image has a {@link FetcherTraitImage::getTargetHeight() logical height} of 20 and a {@link FetcherTraitImage::getTargetWidth() logical width} that is scaled down by the {@link FetcherTraitImage::getIntrinsicAspectRatio() instrinsic ratio}
370*04fd306cSNickeau     *
371*04fd306cSNickeau     * The doc is {@link https://www.dokuwiki.org/images#resizing}
372*04fd306cSNickeau     * @return int
373*04fd306cSNickeau     */
374*04fd306cSNickeau    public function getTargetWidth(): int
375*04fd306cSNickeau    {
376*04fd306cSNickeau
377*04fd306cSNickeau        try {
378*04fd306cSNickeau            return $this->getRequestedWidth();
379*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
380*04fd306cSNickeau            // no requested width
381*04fd306cSNickeau        }
382*04fd306cSNickeau
383*04fd306cSNickeau        /**
384*04fd306cSNickeau         * Scaled down by Height
385*04fd306cSNickeau         */
386*04fd306cSNickeau        try {
387*04fd306cSNickeau            $height = $this->getRequestedHeight();
388*04fd306cSNickeau            try {
389*04fd306cSNickeau                $ratio = $this->getCalculatedRequestedAspectRatioAsFloat();
390*04fd306cSNickeau            } catch (ExceptionNotFound $e) {
391*04fd306cSNickeau                $ratio = $this->getIntrinsicAspectRatio();
392*04fd306cSNickeau            }
393*04fd306cSNickeau            return self::round($ratio * $height);
394*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
395*04fd306cSNickeau            // no requested height
396*04fd306cSNickeau        }
397*04fd306cSNickeau
398*04fd306cSNickeau
399*04fd306cSNickeau        /**
400*04fd306cSNickeau         * Scaled down by Ratio
401*04fd306cSNickeau         */
402*04fd306cSNickeau        try {
403*04fd306cSNickeau            $ratio = $this->getCalculatedRequestedAspectRatioAsFloat();
404*04fd306cSNickeau            [$logicalWidthWithRatio, $logicalHeightWithRatio] = $this->getCroppingDimensionsWithRatio($ratio);
405*04fd306cSNickeau            return $logicalWidthWithRatio;
406*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
407*04fd306cSNickeau            // no ratio requested
408*04fd306cSNickeau        }
409*04fd306cSNickeau
410*04fd306cSNickeau        return $this->getIntrinsicWidth();
411*04fd306cSNickeau
412*04fd306cSNickeau    }
413*04fd306cSNickeau
414*04fd306cSNickeau    /**
415*04fd306cSNickeau     * @return int|null
416*04fd306cSNickeau     * @throws ExceptionNotFound - if no requested width was asked
417*04fd306cSNickeau     */
418*04fd306cSNickeau    public function getRequestedWidth(): int
419*04fd306cSNickeau    {
420*04fd306cSNickeau        if ($this->requestedWidth === null) {
421*04fd306cSNickeau            throw new ExceptionNotFound("No width was requested");
422*04fd306cSNickeau        }
423*04fd306cSNickeau        if ($this->requestedWidth === 0) {
424*04fd306cSNickeau            throw new ExceptionNotFound("Width 0 was requested");
425*04fd306cSNickeau        }
426*04fd306cSNickeau        return $this->requestedWidth;
427*04fd306cSNickeau    }
428*04fd306cSNickeau
429*04fd306cSNickeau    /**
430*04fd306cSNickeau     * @return int
431*04fd306cSNickeau     * @throws ExceptionNotFound - if no requested height was asked
432*04fd306cSNickeau     */
433*04fd306cSNickeau    public function getRequestedHeight(): int
434*04fd306cSNickeau    {
435*04fd306cSNickeau        if ($this->requestedHeight === null) {
436*04fd306cSNickeau            throw new ExceptionNotFound("Height not requested");
437*04fd306cSNickeau        }
438*04fd306cSNickeau        if ($this->requestedHeight === 0) {
439*04fd306cSNickeau            throw new ExceptionNotFound("Height 0 requested");
440*04fd306cSNickeau        }
441*04fd306cSNickeau        return $this->requestedHeight;
442*04fd306cSNickeau    }
443*04fd306cSNickeau
444*04fd306cSNickeau    /**
445*04fd306cSNickeau     * Rounding to integer
446*04fd306cSNickeau     * The fetch.php file takes int as value for width and height
447*04fd306cSNickeau     * making a rounding if we pass a double (such as 37.5)
448*04fd306cSNickeau     * This is important because the security token is based on width and height
449*04fd306cSNickeau     * and therefore the fetch will failed
450*04fd306cSNickeau     *
451*04fd306cSNickeau     * And not directly {@link intval} because it will make from 3.6, 3 and not 4
452*04fd306cSNickeau     *
453*04fd306cSNickeau     * And this is also ask by the specification
454*04fd306cSNickeau     * a non-null positive integer
455*04fd306cSNickeau     * https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
456*04fd306cSNickeau     *
457*04fd306cSNickeau     */
458*04fd306cSNickeau    public static function round(float $param): int
459*04fd306cSNickeau    {
460*04fd306cSNickeau        return intval(round($param));
461*04fd306cSNickeau    }
462*04fd306cSNickeau
463*04fd306cSNickeau
464*04fd306cSNickeau    /**
465*04fd306cSNickeau     *
466*04fd306cSNickeau     * Return the width and height of the image
467*04fd306cSNickeau     * after applying a ratio (16x9, 4x3, ..)
468*04fd306cSNickeau     *
469*04fd306cSNickeau     * The new dimension will apply to:
470*04fd306cSNickeau     *   * the viewBox for svg
471*04fd306cSNickeau     *   * the physical dimension for raster image
472*04fd306cSNickeau     *
473*04fd306cSNickeau     */
474*04fd306cSNickeau    public function getCroppingDimensionsWithRatio(float $targetRatio): array
475*04fd306cSNickeau    {
476*04fd306cSNickeau
477*04fd306cSNickeau        /**
478*04fd306cSNickeau         * Trying to crop on the width
479*04fd306cSNickeau         */
480*04fd306cSNickeau        $logicalWidth = $this->getIntrinsicWidth();
481*04fd306cSNickeau        $logicalHeight = $this->round($logicalWidth / $targetRatio);
482*04fd306cSNickeau        if ($logicalHeight > $this->getIntrinsicHeight()) {
483*04fd306cSNickeau            /**
484*04fd306cSNickeau             * Cropping by height
485*04fd306cSNickeau             */
486*04fd306cSNickeau            $logicalHeight = $this->getIntrinsicHeight();
487*04fd306cSNickeau            $logicalWidth = $this->round($targetRatio * $logicalHeight);
488*04fd306cSNickeau        }
489*04fd306cSNickeau        return [$logicalWidth, $logicalHeight];
490*04fd306cSNickeau
491*04fd306cSNickeau    }
492*04fd306cSNickeau
493*04fd306cSNickeau
494*04fd306cSNickeau    public function setRequestedWidth(int $requestedWidth): FetcherImage
495*04fd306cSNickeau    {
496*04fd306cSNickeau        $this->requestedWidth = $requestedWidth;
497*04fd306cSNickeau        return $this;
498*04fd306cSNickeau    }
499*04fd306cSNickeau
500*04fd306cSNickeau    public function setRequestedHeight(int $requestedHeight): FetcherImage
501*04fd306cSNickeau    {
502*04fd306cSNickeau        $this->requestedHeight = $requestedHeight;
503*04fd306cSNickeau        return $this;
504*04fd306cSNickeau    }
505*04fd306cSNickeau
506*04fd306cSNickeau    /**
507*04fd306cSNickeau     * @throws ExceptionBadSyntax
508*04fd306cSNickeau     */
509*04fd306cSNickeau    public function setRequestedAspectRatio(string $requestedRatio): FetcherImage
510*04fd306cSNickeau    {
511*04fd306cSNickeau        $this->requestedRatio = $requestedRatio;
512*04fd306cSNickeau        $this->requestedRatioAsFloat = Dimension::convertTextualRatioToNumber($requestedRatio);
513*04fd306cSNickeau        return $this;
514*04fd306cSNickeau    }
515*04fd306cSNickeau
516*04fd306cSNickeau
517*04fd306cSNickeau    public function __toString()
518*04fd306cSNickeau    {
519*04fd306cSNickeau        return get_class($this);
520*04fd306cSNickeau    }
521*04fd306cSNickeau
522*04fd306cSNickeau
523*04fd306cSNickeau    public function hasHeightRequested(): bool
524*04fd306cSNickeau    {
525*04fd306cSNickeau        try {
526*04fd306cSNickeau            $this->getRequestedHeight();
527*04fd306cSNickeau            return true;
528*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
529*04fd306cSNickeau            return false;
530*04fd306cSNickeau        }
531*04fd306cSNickeau    }
532*04fd306cSNickeau
533*04fd306cSNickeau    public function hasAspectRatioRequested(): bool
534*04fd306cSNickeau    {
535*04fd306cSNickeau        try {
536*04fd306cSNickeau            $this->getCalculatedRequestedAspectRatioAsFloat();
537*04fd306cSNickeau            return true;
538*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
539*04fd306cSNickeau            return false;
540*04fd306cSNickeau        }
541*04fd306cSNickeau
542*04fd306cSNickeau    }
543*04fd306cSNickeau
544*04fd306cSNickeau
545*04fd306cSNickeau    /**
546*04fd306cSNickeau     * @throws ExceptionNotFound
547*04fd306cSNickeau     */
548*04fd306cSNickeau    public function getRequestedAspectRatio(): string
549*04fd306cSNickeau    {
550*04fd306cSNickeau        if ($this->requestedRatio === null) {
551*04fd306cSNickeau            throw new ExceptionNotFound("No ratio was specified");
552*04fd306cSNickeau        }
553*04fd306cSNickeau        return $this->requestedRatio;
554*04fd306cSNickeau    }
555*04fd306cSNickeau
556*04fd306cSNickeau    public function isCropRequested(): bool
557*04fd306cSNickeau    {
558*04fd306cSNickeau        if ($this->requestedHeight !== null && $this->requestedWidth !== null) {
559*04fd306cSNickeau            return true;
560*04fd306cSNickeau        }
561*04fd306cSNickeau        if ($this->requestedRatio != null) {
562*04fd306cSNickeau            return true;
563*04fd306cSNickeau        }
564*04fd306cSNickeau        return false;
565*04fd306cSNickeau    }
566*04fd306cSNickeau
567*04fd306cSNickeau
568*04fd306cSNickeau}
569