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