diff options
Diffstat (limited to 'lib/private/Avatar/Avatar.php')
-rw-r--r-- | lib/private/Avatar/Avatar.php | 212 |
1 files changed, 92 insertions, 120 deletions
diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index 3bd58bb7681..dc65c9d5743 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -3,54 +3,24 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright 2018 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergey Shliakhov <husband.sergey@gmail.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Avatar; use Imagick; -use OC\Color; -use OC_Image; +use OC\User\User; +use OCP\Color; use OCP\Files\NotFoundException; use OCP\IAvatar; +use OCP\IConfig; use Psr\Log\LoggerInterface; /** * This class gets and sets users avatars. */ abstract class Avatar implements IAvatar { - - /** @var LoggerInterface */ - protected $logger; - /** * https://github.com/sebdesign/cap-height -- for 500px height * Automated check: https://codepen.io/skjnldsv/pen/PydLBK/ @@ -58,30 +28,26 @@ abstract class Avatar implements IAvatar { * (0.4 letter-to-total-height ratio, 500*0.4=200), so: 200/0.715 = 280px. * Since we start from the baseline (text-anchor) we need to * shift the y axis by 100px (half the caps height): 500/2+100=350 - * - * @var string */ - private $svgTemplate = '<?xml version="1.0" encoding="UTF-8" standalone="no"?> + private string $svgTemplate = '<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="{size}" height="{size}" version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg"> <rect width="100%" height="100%" fill="#{fill}"></rect> - <text x="50%" y="350" style="font-weight:normal;font-size:280px;font-family:\'Noto Sans\';text-anchor:middle;fill:#fff">{letter}</text> + <text x="50%" y="350" style="font-weight:normal;font-size:280px;font-family:\'Noto Sans\';text-anchor:middle;fill:#{fgFill}">{letter}</text> </svg>'; - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; + public function __construct( + protected IConfig $config, + protected LoggerInterface $logger, + ) { } /** * Returns the user display name. - * - * @return string */ abstract public function getDisplayName(): string; /** * Returns the first letter of the display name, or "?" if no name given. - * - * @return string */ private function getAvatarText(): string { $displayName = $this->getDisplayName(); @@ -97,16 +63,14 @@ abstract class Avatar implements IAvatar { /** * @inheritdoc */ - public function get($size = 64) { - $size = (int) $size; - + public function get(int $size = 64, bool $darkTheme = false) { try { - $file = $this->getFile($size); + $file = $this->getFile($size, $darkTheme); } catch (NotFoundException $e) { return false; } - $avatar = new OC_Image(); + $avatar = new \OCP\Image(); $avatar->loadFromData($file->getContent()); return $avatar; } @@ -122,70 +86,102 @@ abstract class Avatar implements IAvatar { * @return string * */ - protected function getAvatarVector(int $size): string { - $userDisplayName = $this->getDisplayName(); - $bgRGB = $this->avatarBackgroundColor($userDisplayName); - $bgHEX = sprintf("%02x%02x%02x", $bgRGB->r, $bgRGB->g, $bgRGB->b); + protected function getAvatarVector(string $userDisplayName, int $size, bool $darkTheme): string { + $fgRGB = $this->avatarBackgroundColor($userDisplayName); + $bgRGB = $fgRGB->alphaBlending(0.1, $darkTheme ? new Color(0, 0, 0) : new Color(255, 255, 255)); + $fill = sprintf('%02x%02x%02x', $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); + $fgFill = sprintf('%02x%02x%02x', $fgRGB->red(), $fgRGB->green(), $fgRGB->blue()); $text = $this->getAvatarText(); - $toReplace = ['{size}', '{fill}', '{letter}']; - return str_replace($toReplace, [$size, $bgHEX, $text], $this->svgTemplate); + $toReplace = ['{size}', '{fill}', '{fgFill}', '{letter}']; + return str_replace($toReplace, [$size, $fill, $fgFill, $text], $this->svgTemplate); + } + + /** + * Select the rendering font based on the user's display name and language + */ + private function getFont(string $userDisplayName): string { + if (preg_match('/\p{Han}/u', $userDisplayName) === 1) { + switch ($this->getAvatarLanguage()) { + case 'zh_TW': + return __DIR__ . '/../../../core/fonts/NotoSansTC-Regular.ttf'; + case 'zh_HK': + return __DIR__ . '/../../../core/fonts/NotoSansHK-Regular.ttf'; + case 'ja': + return __DIR__ . '/../../../core/fonts/NotoSansJP-Regular.ttf'; + case 'ko': + return __DIR__ . '/../../../core/fonts/NotoSansKR-Regular.ttf'; + default: + return __DIR__ . '/../../../core/fonts/NotoSansSC-Regular.ttf'; + } + } + return __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; } /** * Generate png avatar from svg with Imagick - * - * @param int $size - * @return string|boolean */ - protected function generateAvatarFromSvg(int $size) { + protected function generateAvatarFromSvg(string $userDisplayName, int $size, bool $darkTheme): ?string { if (!extension_loaded('imagick')) { - return false; + return null; } + $formats = Imagick::queryFormats(); + // Avatar generation breaks if RSVG format is enabled. Fall back to gd in that case + if (in_array('RSVG', $formats, true)) { + return null; + } + $text = $this->getAvatarText(); try { - $font = __DIR__ . '/../../core/fonts/NotoSans-Regular.ttf'; - $svg = $this->getAvatarVector($size); + $font = $this->getFont($text); + $svg = $this->getAvatarVector($userDisplayName, $size, $darkTheme); $avatar = new Imagick(); $avatar->setFont($font); $avatar->readImageBlob($svg); $avatar->setImageFormat('png'); - $image = new OC_Image(); + $image = new \OCP\Image(); $image->loadFromData((string)$avatar); - $data = $image->data(); - return $data === null ? false : $data; + return $image->data(); } catch (\Exception $e) { - return false; + return null; } } /** * Generate png avatar with GD - * - * @param string $userDisplayName - * @param int $size - * @return string + * @throws \Exception when an error occurs in gd calls */ - protected function generateAvatar($userDisplayName, $size) { + protected function generateAvatar(string $userDisplayName, int $size, bool $darkTheme): string { $text = $this->getAvatarText(); - $backgroundColor = $this->avatarBackgroundColor($userDisplayName); + $textColor = $this->avatarBackgroundColor($userDisplayName); + $backgroundColor = $textColor->alphaBlending(0.1, $darkTheme ? new Color(0, 0, 0) : new Color(255, 255, 255)); $im = imagecreatetruecolor($size, $size); + if ($im === false) { + throw new \Exception('Failed to create avatar image'); + } $background = imagecolorallocate( $im, - $backgroundColor->r, - $backgroundColor->g, - $backgroundColor->b + $backgroundColor->red(), + $backgroundColor->green(), + $backgroundColor->blue() + ); + $textColor = imagecolorallocate($im, + $textColor->red(), + $textColor->green(), + $textColor->blue() ); - $white = imagecolorallocate($im, 255, 255, 255); + if ($background === false || $textColor === false) { + throw new \Exception('Failed to create avatar image color'); + } imagefilledrectangle($im, 0, 0, $size, $size, $background); - $font = __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; + $font = $this->getFont($text); $fontSize = $size * 0.4; [$x, $y] = $this->imageTTFCenter( $im, $text, $font, (int)$fontSize ); - imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $text); + imagettftext($im, $fontSize, 0, $x, $y, $textColor, $font, $text); ob_start(); imagepng($im); @@ -198,7 +194,7 @@ abstract class Avatar implements IAvatar { /** * Calculate real image ttf center * - * @param resource $image + * @param \GdImage $image * @param string $text text string * @param string $font font path * @param int $size font size @@ -210,7 +206,7 @@ abstract class Avatar implements IAvatar { string $text, string $font, int $size, - $angle = 0 + int $angle = 0, ): array { // Image width & height $xi = imagesx($image); @@ -230,37 +226,6 @@ abstract class Avatar implements IAvatar { return [$x, $y]; } - /** - * Calculate steps between two Colors - * @param object Color $steps start color - * @param object Color $ends end color - * @return array [r,g,b] steps for each color to go from $steps to $ends - */ - private function stepCalc($steps, $ends) { - $step = []; - $step[0] = ($ends[1]->r - $ends[0]->r) / $steps; - $step[1] = ($ends[1]->g - $ends[0]->g) / $steps; - $step[2] = ($ends[1]->b - $ends[0]->b) / $steps; - return $step; - } - - /** - * Convert a string to an integer evenly - * @param string $hash the text to parse - * @param int $maximum the maximum range - * @return int[] between 0 and $maximum - */ - private function mixPalette($steps, $color1, $color2) { - $palette = [$color1]; - $step = $this->stepCalc($steps, [$color1, $color2]); - for ($i = 1; $i < $steps; $i++) { - $r = intval($color1->r + ($step[0] * $i)); - $g = intval($color1->g + ($step[1] * $i)); - $b = intval($color1->b + ($step[2] * $i)); - $palette[] = new Color($r, $g, $b); - } - return $palette; - } /** * Convert a string to an integer evenly @@ -268,7 +233,7 @@ abstract class Avatar implements IAvatar { * @param int $maximum the maximum range * @return int between 0 and $maximum */ - private function hashToInt($hash, $maximum) { + private function hashToInt(string $hash, int $maximum): int { $final = 0; $result = []; @@ -286,10 +251,9 @@ abstract class Avatar implements IAvatar { } /** - * @param string $hash - * @return Color Object containting r g b int in the range [0, 255] + * @return Color Object containing r g b int in the range [0, 255] */ - public function avatarBackgroundColor(string $hash) { + public function avatarBackgroundColor(string $hash): Color { // Normalize hash $hash = strtolower($hash); @@ -309,12 +273,20 @@ abstract class Avatar implements IAvatar { // 3 colors * 6 will result in 18 generated colors $steps = 6; - $palette1 = $this->mixPalette($steps, $red, $yellow); - $palette2 = $this->mixPalette($steps, $yellow, $blue); - $palette3 = $this->mixPalette($steps, $blue, $red); + $palette1 = Color::mixPalette($steps, $red, $yellow); + $palette2 = Color::mixPalette($steps, $yellow, $blue); + $palette3 = Color::mixPalette($steps, $blue, $red); $finalPalette = array_merge($palette1, $palette2, $palette3); return $finalPalette[$this->hashToInt($hash, $steps * 3)]; } + + /** + * Get the language to be used for avatar generation. + * This is used to determine the font to use for the avatar text (e.g. CJK characters). + */ + protected function getAvatarLanguage(): string { + return $this->config->getSystemValueString('default_language', 'en'); + } } |