1<?php
2
3
4namespace ComboStrap;
5
6use ComboStrap\Web\Url;
7
8/**
9 *
10 * A Image Raster processing class that:
11 *   * takes as input:
12 *      * a {@link FetcherRaster::buildFromUrl() fetch URL}:
13 *          * from an HTTP request
14 *          * or {@link MediaMarkup::getFetchUrl() markup})
15 *      * or data via setter
16 *   * outputs:
17 *      * a {@link FetcherRaster::getFetchPath() raster image file} for:
18 *         * an HTTP response
19 *         * or further local processing
20 *      * or a {@link FetcherRaster::getFetchUrl() fetch url} to use in a {@link RasterImageLink img html tag}
21 *
22 */
23class FetcherRaster extends IFetcherLocalImage
24{
25
26    use FetcherTraitWikiPath {
27        setSourcePath as protected setOriginalPathTrait;
28    }
29
30    const CANONICAL = "raster";
31    const FAKE_LENGTH_FOR_BROKEN_IMAGES = 10;
32
33
34    private int $imageWidth;
35    private int $imageWeight;
36
37
38    /**
39     * @param string $imageId
40     * @param null $rev
41     * @return FetcherRaster
42     * @throws ExceptionBadArgument
43     * @throws ExceptionBadSyntax
44     * @throws ExceptionNotExists
45     */
46    public static function createImageRasterFetchFromId(string $imageId, $rev = null): FetcherRaster
47    {
48        return FetcherRaster::createImageRasterFetchFromPath(WikiPath::createMediaPathFromId($imageId, $rev));
49    }
50
51    /**
52     * @param Path $path
53     * @return FetcherRaster
54     * @throws ExceptionBadArgument
55     * @throws ExceptionBadSyntax
56     * @throws ExceptionNotExists
57     */
58    public static function createImageRasterFetchFromPath(Path $path): FetcherRaster
59    {
60        $path = WikiPath::createFromPathObject($path);
61        return self::createEmptyRaster()
62            ->setSourcePath($path);
63    }
64
65    public static function createEmptyRaster(): FetcherRaster
66    {
67        return new FetcherRaster();
68    }
69
70    /**
71     * @throws ExceptionBadArgument
72     */
73    public static function createRasterFromFetchUrl(Url $fetchUrl): FetcherRaster
74    {
75        $fetchImageRaster = self::createEmptyRaster();
76        $fetchImageRaster->buildFromUrl($fetchUrl);
77        return $fetchImageRaster;
78    }
79
80
81    /**
82     * @return int - the width of the image from the file
83     */
84    public function getIntrinsicWidth(): int
85    {
86        return $this->imageWidth;
87    }
88
89    public function getFetchUrl(Url $url = null): Url
90    {
91        /**
92         *
93         * Because {@link FetcherRaster} does not create the image itself
94         * but dokuwiki does, we need to add the with and height dimension
95         * if the ratio is asked
96         *
97         * Before all other parent requirement such as
98         * ({@link FetcherImage::getTok()} uses them
99         *
100         * Note that we takes the target value
101         * before setting them otherwise it will affect the calculcation
102         * ie if we set the height and then calculatiing the target width, we will get
103         * a mini difference
104         *
105         */
106        try {
107            $this->getRequestedAspectRatio();
108            $targetHeight = $this->getTargetHeight();
109            $targetWidth = $this->getTargetWidth();
110            $this->setRequestedWidth($targetWidth);
111            $this->setRequestedHeight($targetHeight);
112        } catch (ExceptionNotFound $e) {
113            //
114        }
115
116        $url = parent::getFetchUrl($url);
117
118        /**
119         * Trait
120         */
121        $this->addLocalPathParametersToFetchUrl($url, MediaMarkup::$MEDIA_QUERY_PARAMETER);
122
123        return $url;
124    }
125
126
127    /**
128     * @return int - the height of the image from the file
129     */
130    public function getIntrinsicHeight(): int
131    {
132        return $this->imageWeight;
133    }
134
135    /**
136     * We check the existence of the file at build time
137     * because when we build the url, we make it at several breakpoints.
138     * We therefore needs the intrinsic dimension (height and weight)
139     *
140     * @throws ExceptionBadSyntax - if the path is not valid image format
141     */
142    private
143    function analyzeImageIfNeeded()
144    {
145
146        if (!FileSystems::exists($this->getSourcePath())) {
147            // The user may type a bad path
148            // We don't throw as we want to be able to build
149            LogUtility::warning("The path ({$this->getSourcePath()}) does not exists");
150            // broken image in the browser does not have any dimension
151            // todo: https://bitsofco.de/styling-broken-images/
152            $this->imageWidth = self::FAKE_LENGTH_FOR_BROKEN_IMAGES;
153            $this->imageWeight = self::FAKE_LENGTH_FOR_BROKEN_IMAGES;
154            return;
155        }
156
157        /**
158         * Based on {@link media_image_preview_size()}
159         * $dimensions = media_image_preview_size($this->id, '', false);
160         */
161        $path = $this->getSourcePath()->toLocalPath();
162        $imageSize = getimagesize($path->toAbsolutePath()->toAbsoluteId());
163        if ($imageSize === false) {
164            throw new ExceptionBadSyntax("We couldn't retrieve the type and dimensions of the image ($this). The image format seems to be not supported.", self::CANONICAL);
165        }
166        $this->imageWidth = (int)$imageSize[0];
167        if (empty($this->imageWidth)) {
168            throw new ExceptionBadSyntax("We couldn't retrieve the width of the image ($this)", self::CANONICAL);
169        }
170        $this->imageWeight = (int)$imageSize[1];
171        if (empty($this->imageWeight)) {
172            throw new ExceptionBadSyntax("We couldn't retrieve the height of the image ($this)", self::CANONICAL);
173        }
174
175    }
176
177
178    /**
179     * We overwrite the {@link FetcherTraitImage::getRequestedWidth()}
180     * because we don't scale up for raster image
181     * to not lose quality.
182     *
183     * @return int
184     * @throws ExceptionNotFound
185     */
186    public
187    function getRequestedWidth(): int
188    {
189
190        /**
191         * Test, requested width should not be bigger than the media Height
192         * If this is the case, we return the media width
193         */
194        $requestedWidth = parent::getRequestedWidth();
195
196        /**
197         * A width was requested
198         */
199        $mediaWidth = $this->getIntrinsicWidth();
200        if ($requestedWidth > $mediaWidth) {
201            global $ID;
202            if ($ID !== "wiki:syntax") {
203                /**
204                 * Info and not warning level because they fill the error log
205                 * They don't really break anything and it's difficult
206                 * to see when it's intended (ie there is no better image or not)
207                 */
208                // There is a bug in the wiki syntax page
209                // {{wiki:dokuwiki-128.png?200x50}}
210                // https://forum.dokuwiki.org/d/19313-bugtypo-how-to-make-a-request-to-change-the-syntax-page-on-dokuwikii
211                LogUtility::info("For the image ($this), the requested width of ($requestedWidth) can not be bigger than the intrinsic width of ($mediaWidth). The width was then set to its natural width ($mediaWidth)", self::CANONICAL);
212            }
213            return $mediaWidth;
214        }
215
216        return $requestedWidth;
217
218    }
219
220
221    /**
222     *
223     */
224    public
225    function getTargetHeight(): int
226    {
227
228        try {
229            $requestedHeight = $this->getRequestedHeight();
230
231            // it should not be bigger than the media Height
232            $mediaHeight = $this->getIntrinsicHeight();
233            if ($requestedHeight > $mediaHeight) {
234                /**
235                 * Info and not warning level because they fill the error log
236                 * They don't really break anything and it's difficult
237                 * to see when it's intended (ie there is no better image or not)
238                 */
239                LogUtility::info("For the image ($this), the requested height of ($requestedHeight) can not be bigger than the intrinsic height of ($mediaHeight). The height was then set to its natural height ($mediaHeight)", self::CANONICAL);
240                return $mediaHeight;
241            }
242        } catch (ExceptionNotFound $e) {
243            // no request height
244        }
245        return parent::getTargetHeight();
246
247
248    }
249
250    public function getTargetWidth(): int
251    {
252        $targetWidth = parent::getTargetWidth();
253        $intrinsicWidth = $this->getIntrinsicWidth();
254        if ($targetWidth > $intrinsicWidth) {
255            /**
256             * Info and not warning level because they fill the error log
257             * They don't really break anything and it's difficult
258             * to see when it's intended (ie there is no better image or not)
259             */
260            LogUtility::info("For the image ($this), the calculated width of ($targetWidth) cannot be bigger than the intrinsic width of ($targetWidth). The requested width was then set to its natural width ($intrinsicWidth).", self::CANONICAL);
261            return $intrinsicWidth;
262        }
263        return $targetWidth;
264    }
265
266
267    function getFetchPath(): LocalPath
268    {
269        /**
270         * In fetch.php
271         * if($HEIGHT && $WIDTH) {
272         *    $data['file'] = $FILE = media_crop_image($data['file'], $EXT, $WIDTH, $HEIGHT);
273         * } else {
274         *    $data['file'] = $FILE = media_resize_image($data['file'], $EXT, $WIDTH, $HEIGHT);
275         * }
276         */
277        throw new ExceptionRuntime("Fetch Raster image is not yet implemented");
278    }
279
280
281    /**
282     * @param TagAttributes $tagAttributes
283     * @return FetcherRaster
284     * @throws ExceptionBadArgument - if the path is not an image
285     * @throws ExceptionBadSyntax - if the image is badly encoded
286     * @throws ExceptionNotExists - if the image does not exists
287     */
288
289    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherImage
290    {
291
292        parent::buildFromTagAttributes($tagAttributes);
293        $this->buildOriginalPathFromTagAttributes($tagAttributes);
294        $this->analyzeImageIfNeeded();
295        return $this;
296
297    }
298
299    /**
300     * @throws ExceptionBadSyntax - if the file is badly encoded
301     * @throws ExceptionNotExists - if the file does not exists
302     */
303    public function setSourcePath(WikiPath $path): FetcherRaster
304    {
305        $this->setOriginalPathTrait($path);
306        $this->analyzeImageIfNeeded();
307        return $this;
308    }
309
310
311    public function getFetcherName(): string
312    {
313        return self::CANONICAL;
314    }
315
316    public function __toString()
317    {
318        return $this->getSourcePath()->__toString();
319    }
320
321    /**
322     * @return int
323     * @throws ExceptionNotFound
324     * We can upscale, we limit then the requested height to the internal size
325     */
326    public function getRequestedHeight(): int
327    {
328        $requestedHeight = parent::getRequestedHeight();
329        $intrinsicHeight = $this->getIntrinsicHeight();
330        if ($requestedHeight > $intrinsicHeight) {
331            /**
332             * Info and not warning to not fill the log
333             * as it's pretty common with a {@link PageImageTag}
334             */
335            LogUtility::info("For the image ($this), the requested height of ($requestedHeight) can not be bigger than the intrinsic height of ($intrinsicHeight). The height was then set to its natural height ($intrinsicHeight)", self::CANONICAL);
336            return $intrinsicHeight;
337        }
338        return $requestedHeight;
339    }
340
341
342}
343