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