1<?php /** @noinspection PhpComposerExtensionStubsInspection */
2
3namespace ComboStrap;
4
5use ComboStrap\Web\Url;
6use ComboStrap\Web\UrlRewrite;
7
8/**
9 *
10 * Vignette:
11 * http://host/lib/exe/fetch.php?media=id:of:page.png&drive=page-vignette
12 * where:
13 *   * 'id:of:page' is the page wiki id
14 *   * 'png' is the format (may be jpeg or webp)
15 *
16 * Example when running on Combo
17 * http://combo.nico.lan/lib/exe/fetch.php?media=howto:getting_started:getting_started.png&drive=page-vignette
18 * http://combo.nico.lan/lib/exe/fetch.php?media=howto:howto.webp&drive=page-vignette
19 *
20 *
21 * Example/Inspiration in the real world:
22 * https://lofi.limo/blog/images/write-html-right.png
23 * https://opengraph.githubassets.com/6b85042cdc8e98725bd85a0e7b159c99104644fbf97402fded205ee4d2036ab9/ComboStrap/combo
24 */
25class FetcherVignette extends FetcherImage
26{
27
28
29    const CANONICAL = self::VIGNETTE_FETCHER_NAME;
30
31    /**
32     * For {@link UrlRewrite}, the property id
33     * should be called media
34     */
35    const MEDIA_NAME_URL_ATTRIBUTE = "media";
36    const PNG_EXTENSION = "png";
37    const JPG_EXTENSION = "jpg";
38    const JPEG_EXTENSION = "jpeg";
39    const WEBP_EXTENSION = "webp";
40
41    const VIGNETTE_FETCHER_NAME = "vignette";
42
43
44    private ?MarkupPath $page = null;
45
46    private Mime $mime;
47
48
49    private string $buster;
50
51    private WikiPath $pagePath;
52
53
54    /**
55     * @throws ExceptionNotFound - if the page does not exists
56     * @throws ExceptionBadArgument - if the mime is not supported or the path of the page is not a wiki path
57     */
58    public static function createForPage(MarkupPath $page, Mime $mime = null): FetcherVignette
59    {
60        $fetcherVignette = new FetcherVignette();
61        $fetcherVignette->setPage($page);
62        if ($mime === null) {
63            $mime = Mime::create(Mime::WEBP);
64        }
65        $fetcherVignette->setMime($mime);
66        return $fetcherVignette;
67
68    }
69
70    /**
71     *
72     * @throws ExceptionBadArgument
73     */
74    public function getFetchPath(): LocalPath
75    {
76
77        $extension = $this->mime->getExtension();
78        $cache = new FetcherCache($this);
79
80        /**
81         * Building the cache dependencies
82         */
83        try {
84            $cache->addFileDependency($this->page->getPathObject())
85                ->addFileDependency(ClassUtility::getClassPath($this));
86        } catch (\ReflectionException $e) {
87            // It should not happen but yeah
88            LogUtility::internalError("The path of the actual class cannot be determined", self::CANONICAL);
89        }
90
91        /**
92         * Can we use the cache ?
93         */
94        if ($cache->isCacheUsable()) {
95            return LocalPath::createFromPathObject($cache->getFile());
96        }
97
98        $width = $this->getIntrinsicWidth();
99        $height = $this->getIntrinsicHeight();
100
101        /**
102         * Don't use {@link imagecreate()} otherwise
103         * we get color problem while importing the logo
104         */
105        $vignetteImageHandler = imagecreatetruecolor($width, $height);
106        try {
107
108            /**
109             * Background
110             * The first call to  {@link imagecolorallocate} fills the background color in palette-based images
111             */
112            $whiteGdColor = imagecolorallocate($vignetteImageHandler, 255, 255, 255);
113            imagefill($vignetteImageHandler, 0, 0, $whiteGdColor);
114
115            /**
116             * Common variable
117             */
118            $margin = 80;
119            $x = $margin;
120            $normalFont = Font::getLiberationSansFontRegularPath()->toAbsoluteId();
121            $boldFont = Font::getLiberationSansFontBoldPath()->toAbsoluteId();
122            try {
123                $mutedRgb = ColorRgb::createFromString("gray");
124                $blackGdColor = imagecolorallocate($vignetteImageHandler, 0, 0, 0);
125                $mutedGdColor = imagecolorallocate($vignetteImageHandler, $mutedRgb->getRed(), $mutedRgb->getGreen(), $mutedRgb->getBlue());
126            } catch (ExceptionCompile $e) {
127                // internal error, should not happen
128                throw new ExceptionBadArgument("Error while getting the muted color. Error: {$e->getMessage()}", self::CANONICAL);
129            }
130
131            /**
132             * Category
133             */
134            try {
135                $parentPage = $this->page->getParent();
136                $yCategory = 120;
137                $categoryFontSize = 40;
138                $lineToPrint = $parentPage->getNameOrDefault();
139                imagettftext($vignetteImageHandler, $categoryFontSize, 0, $x, $yCategory, $mutedGdColor, $normalFont, $lineToPrint);
140            } catch (ExceptionNotFound $e) {
141                // No parent
142            }
143
144
145            /**
146             * Title
147             */
148            $title = trim($this->page->getTitleOrDefault());
149            $titleFontSize = 55;
150            $yTitleStart = 210;
151            $yTitleActual = $yTitleStart;
152            $lineSpace = 25;
153            $words = explode(" ", $title);
154            $maxCharacterByLine = 20;
155            $actualLine = "";
156            $lineCount = 0;
157            $maxNumberOfLines = 3;
158            $break = false;
159            foreach ($words as $word) {
160                $actualLength = strlen($actualLine);
161                if ($actualLength + strlen($word) > $maxCharacterByLine) {
162                    $lineCount = $lineCount + 1;
163                    $lineToPrint = $actualLine;
164                    if ($lineCount >= $maxNumberOfLines) {
165                        $lineToPrint = $actualLine . "...";
166                        $actualLine = "";
167                        $break = true;
168                    } else {
169                        $actualLine = $word;
170                    }
171                    imagettftext($vignetteImageHandler, $titleFontSize, 0, $x, $yTitleActual, $blackGdColor, $boldFont, $lineToPrint);
172                    $yTitleActual = $yTitleActual + $titleFontSize + $lineSpace;
173                    if ($break) {
174                        break;
175                    }
176                } else {
177                    if ($actualLine === "") {
178                        $actualLine = $word;
179                    } else {
180                        $actualLine = "$actualLine $word";
181                    }
182                }
183            }
184            if ($actualLine !== "") {
185                imagettftext($vignetteImageHandler, $titleFontSize, 0, $x, $yTitleActual, $blackGdColor, $boldFont, $actualLine);
186            }
187
188            /**
189             * Date
190             */
191            $yDate = $yTitleStart + 3 * ($titleFontSize + $lineSpace) + 2 * $lineSpace;
192            $dateFontSize = 30;
193            $mutedGdColor = imagecolorallocate($vignetteImageHandler, $mutedRgb->getRed(), $mutedRgb->getGreen(), $mutedRgb->getBlue());
194            $locale = Locale::createForPage($this->page)->getValueOrDefault();
195            try {
196                $modifiedTimeOrDefault = $this->page->getModifiedTimeOrDefault();
197            } catch (ExceptionNotFound $e) {
198                LogUtility::errorIfDevOrTest("Error while getting the modified date. Error: {$e->getMessage()}", self::CANONICAL);
199                $modifiedTimeOrDefault = new \DateTime();
200            }
201            try {
202                $lineToPrint = Iso8601Date::createFromDateTime($modifiedTimeOrDefault)->formatLocale(null, $locale);
203            } catch (ExceptionBadSyntax $e) {
204                // should not happen
205                LogUtility::errorIfDevOrTest("Error while formatting the modified date. Error: {$e->getMessage()}", self::CANONICAL);
206                $lineToPrint = $modifiedTimeOrDefault->format('Y-m-d H:i:s');
207            }
208            imagettftext($vignetteImageHandler, $dateFontSize, 0, $x, $yDate, $mutedGdColor, $normalFont, $lineToPrint);
209
210            /**
211             * Logo
212             */
213            try {
214
215                $imagePath = Site::getLogoAsRasterImage()->getSourcePath();
216                $gdOriginalLogo = $this->getGdImageHandler($imagePath);
217                $targetLogoWidth = 120;
218                $targetLogoHandler = imagescale($gdOriginalLogo, $targetLogoWidth);
219                imagecopy($vignetteImageHandler, $targetLogoHandler, 950, 130, 0, 0, $targetLogoWidth, imagesy($targetLogoHandler));
220
221            } catch (ExceptionNotFound $e) {
222                // no logo installed, mime not found, extension not supported
223                LogUtility::warning("The vignette could not be created with your logo because of the following error: {$e->getMessage()}");
224            }
225
226            /**
227             * Store
228             */
229            $fileStringPath = $cache->getFile()->toAbsolutePath()->toAbsoluteId();
230            switch ($extension) {
231                case self::PNG_EXTENSION:
232                    imagetruecolortopalette($vignetteImageHandler, false, 255);
233                    imagepng($vignetteImageHandler, $fileStringPath);
234                    break;
235                case self::JPG_EXTENSION:
236                case self::JPEG_EXTENSION:
237                    imagejpeg($vignetteImageHandler, $fileStringPath);
238                    break;
239                case self::WEBP_EXTENSION:
240                    /**
241                     * To True Color to avoid:
242                     * `
243                     * Fatal error: Palette image not supported by webp
244                     * `
245                     */
246                    imagewebp($vignetteImageHandler, $fileStringPath);
247                    break;
248                default:
249                    LogUtility::internalError("The possible mime error should have been caught in the setter");
250            }
251
252        } finally {
253            imagedestroy($vignetteImageHandler);
254        }
255
256
257        return $cache->getFile();
258    }
259
260    public function setUseCache(bool $false): FetcherVignette
261    {
262        $this->useCache = $false;
263        return $this;
264    }
265
266    public function getIntrinsicWidth(): int
267    {
268        return 1200;
269    }
270
271    public function getIntrinsicHeight(): int
272    {
273        return 600;
274    }
275
276
277    /**
278     * @throws ExceptionNotFound - unknown mime or unknown extension
279     */
280    private function getGdImageHandler(WikiPath $imagePath)
281    {
282        // the gd function needs a local path, not a wiki path
283        $imagePath = $imagePath->toLocalPath();
284        $extension = FileSystems::getMime($imagePath)->getExtension();
285
286        switch ($extension) {
287            case self::PNG_EXTENSION:
288                return imagecreatefrompng($imagePath->toAbsoluteId());
289            case self::JPG_EXTENSION:
290            case self::JPEG_EXTENSION:
291                return imagecreatefromjpeg($imagePath->toAbsoluteId());
292            case self::WEBP_EXTENSION:
293                return imagecreatefromwebp($imagePath->toAbsoluteId());
294            default:
295                throw new ExceptionNotFound("Bad mime should have been caught by the setter");
296        }
297
298    }
299
300
301    function getFetchUrl(Url $url = null): Url
302    {
303
304        $vignetteNameValue = $this->pagePath->getWikiId() . "." . $this->mime->getExtension();
305        return parent::getFetchUrl($url)
306            ->addQueryParameter(self::MEDIA_NAME_URL_ATTRIBUTE, $vignetteNameValue);
307
308    }
309
310
311    function getBuster(): string
312    {
313        return $this->buster;
314    }
315
316
317    public function getMime(): Mime
318    {
319        return $this->mime;
320    }
321
322    /**
323     * @throws ExceptionBadArgument
324     * @throws ExceptionNotFound
325     */
326    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherVignette
327    {
328
329        $vignette = $tagAttributes->getValueAndRemove(self::MEDIA_NAME_URL_ATTRIBUTE);
330        if ($vignette === null && $this->page === null) {
331            throw new ExceptionBadArgument("The vignette query property is mandatory when the vignette was created without page.");
332        }
333
334        if ($vignette !== null) {
335            $lastPoint = strrpos($vignette, ".");
336            $extension = substr($vignette, $lastPoint + 1);
337            $wikiId = substr($vignette, 0, $lastPoint);
338            $this->setPage(MarkupPath::createMarkupFromId($wikiId));
339            if (!FileSystems::exists($this->page->getPathObject())) {
340                throw new ExceptionNotFound("The page does not exists");
341            }
342            try {
343                $this->setMime(Mime::createFromExtension($extension));
344            } catch (ExceptionNotFound $e) {
345                throw new ExceptionBadArgument("The vignette mime is unknown. Error: {$e->getMessage()}");
346            }
347        }
348
349        parent::buildFromTagAttributes($tagAttributes);
350        return $this;
351
352    }
353
354
355    public function getFetcherName(): string
356    {
357        return self::VIGNETTE_FETCHER_NAME;
358    }
359
360    /**
361     * @throws ExceptionNotFound
362     * @throws ExceptionBadArgument - if the markup path is not
363     */
364    public function setPage(MarkupPath $page): FetcherVignette
365    {
366        $this->page = $page;
367        $path = $this->page->getPathObject();
368        if (!($path instanceof WikiPath)) {
369            if ($path instanceof LocalPath) {
370                $path = $path->toWikiPath();
371            } else {
372                throw new ExceptionBadArgument("The path of the markup file is not a wiki path and could not be transformed.");
373            }
374        }
375        $this->pagePath = $path;
376        $this->buster = FileSystems::getCacheBuster($path);
377        return $this;
378    }
379
380    /**
381     * @throws ExceptionBadArgument
382     */
383    public function setMime(Mime $mime): FetcherVignette
384    {
385        $this->mime = $mime;
386        $gdInfo = gd_info();
387        $extension = $mime->getExtension();
388        switch ($extension) {
389            case self::PNG_EXTENSION:
390                if (!$gdInfo["PNG Support"]) {
391                    throw new ExceptionBadArgument("The extension ($extension) is not supported by the GD library", self::CANONICAL);
392                }
393                break;
394            case self::JPG_EXTENSION:
395            case self::JPEG_EXTENSION:
396                if (!$gdInfo["JPEG Support"]) {
397                    throw new ExceptionBadArgument("The extension ($extension) is not supported by the GD library", self::CANONICAL);
398                }
399                break;
400            case self::WEBP_EXTENSION:
401                if (!$gdInfo["WebP Support"]) {
402                    throw new ExceptionBadArgument("The extension ($extension) is not supported by the GD library", self::CANONICAL);
403                }
404                break;
405            default:
406                throw new ExceptionBadArgument("The mime ($mime) is not supported");
407        }
408        return $this;
409    }
410
411    public function getLabel(): string
412    {
413        return ResourceName::getFromPath($this->pagePath);
414    }
415}
416