From f98ae2b5b0567f28a875106724c0475d6395f3c5 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 30 Aug 2022 13:27:21 +0200 Subject: [PATCH] Avatar new style Signed-off-by: Carl Schwan --- core/Controller/AvatarController.php | 40 +++++++++++++++++++++++- core/routes.php | 1 + lib/private/Avatar/Avatar.php | 37 +++++++++++++--------- lib/private/Avatar/UserAvatar.php | 46 +++++++++++++++++++--------- lib/public/Color.php | 8 +++++ lib/public/IAvatar.php | 6 ++-- 6 files changed, 105 insertions(+), 33 deletions(-) diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 5fcd2a9abbe5b..804000bf2a754 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -84,6 +84,44 @@ public function __construct(string $appName, $this->timeFactory = $timeFactory; } + /** + * @NoAdminRequired + * @NoCSRFRequired + * @NoSameSiteCookieRequired + * @PublicPage + * + * @return JSONResponse|FileDisplayResponse + */ + public function getAvatarDark(string $userId, int $size) { + if ($size <= 64) { + if ($size !== 64) { + $this->logger->debug('Avatar requested in deprecated size ' . $size); + } + $size = 64; + } else { + if ($size !== 512) { + $this->logger->debug('Avatar requested in deprecated size ' . $size); + } + $size = 512; + } + + try { + $avatar = $this->avatarManager->getAvatar($userId); + $avatarFile = $avatar->getFile($size, true); + $response = new FileDisplayResponse( + $avatarFile, + Http::STATUS_OK, + ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] + ); + } catch (\Exception $e) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + // Cache for 1 day + $response->cacheFor(60 * 60 * 24, false, true); + return $response; + } + /** * @NoAdminRequired @@ -93,7 +131,7 @@ public function __construct(string $appName, * * @return JSONResponse|FileDisplayResponse */ - public function getAvatar(string $userId, int $size) { + public function getAvatar(string $userId, int $size, bool $darkTheme = false) { if ($size <= 64) { if ($size !== 64) { $this->logger->debug('Avatar requested in deprecated size ' . $size); diff --git a/core/routes.php b/core/routes.php index b75cb0f6b3b44..ee5fbb34a4c74 100644 --- a/core/routes.php +++ b/core/routes.php @@ -45,6 +45,7 @@ ['name' => 'lost#setPassword', 'url' => '/lostpassword/set/{token}/{userId}', 'verb' => 'POST'], ['name' => 'ProfilePage#index', 'url' => '/u/{targetUserId}', 'verb' => 'GET'], ['name' => 'user#getDisplayNames', 'url' => '/displaynames', 'verb' => 'POST'], + ['name' => 'avatar#getAvatarDark', 'url' => '/avatar/{userId}/{size}/dark', 'verb' => 'GET'], ['name' => 'avatar#getAvatar', 'url' => '/avatar/{userId}/{size}', 'verb' => 'GET'], ['name' => 'avatar#deleteAvatar', 'url' => '/avatar/', 'verb' => 'DELETE'], ['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'], diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index 0eb8f8816d826..aec0f6e10f4a9 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -59,7 +59,7 @@ abstract class Avatar implements IAvatar { private string $svgTemplate = ' - {letter} + {letter} '; public function __construct(LoggerInterface $logger) { @@ -88,9 +88,9 @@ private function getAvatarText(): string { /** * @inheritdoc */ - public function get(int $size = 64) { + public function get(int $size = 64, bool $darkTheme = false) { try { - $file = $this->getFile($size); + $file = $this->getFile($size, $darkTheme); } catch (NotFoundException $e) { return false; } @@ -111,25 +111,27 @@ public function get(int $size = 64) { * @return string * */ - protected function getAvatarVector(int $size): string { + protected function getAvatarVector(int $size, bool $dark): string { $userDisplayName = $this->getDisplayName(); - $bgRGB = $this->avatarBackgroundColor($userDisplayName); - $bgHEX = sprintf("%02x%02x%02x", $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); + $fgRGB = $this->avatarBackgroundColor($userDisplayName); + $bgRGB = $fgRGB->alphaBlending(0.1, $dark ? 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); } /** * Generate png avatar from svg with Imagick */ - protected function generateAvatarFromSvg(int $size): ?string { + protected function generateAvatarFromSvg(int $size, bool $dark): ?string { if (!extension_loaded('imagick')) { return null; } try { - $font = __DIR__ . '/../../core/fonts/NotoSans-Regular.ttf'; - $svg = $this->getAvatarVector($size); + $font = __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; + $svg = $this->getAvatarVector($size, $dark); $avatar = new Imagick(); $avatar->setFont($font); $avatar->readImageBlob($svg); @@ -145,9 +147,10 @@ protected function generateAvatarFromSvg(int $size): ?string { /** * Generate png avatar with GD */ - protected function generateAvatar(string $userDisplayName, int $size): string { + protected function generateAvatar(string $userDisplayName, int $size, bool $dark): string { $text = $this->getAvatarText(); - $backgroundColor = $this->avatarBackgroundColor($userDisplayName); + $textColor = $this->avatarBackgroundColor($userDisplayName); + $backgroundColor = $textColor->alphaBlending(0.1, $dark ? new Color() : new Color(255, 255, 255)); $im = imagecreatetruecolor($size, $size); $background = imagecolorallocate( @@ -156,7 +159,11 @@ protected function generateAvatar(string $userDisplayName, int $size): string { $backgroundColor->green(), $backgroundColor->blue() ); - $white = imagecolorallocate($im, 255, 255, 255); + $textColor = imagecolorallocate($im, + $textColor->red(), + $textColor->green(), + $textColor->blue() + ); imagefilledrectangle($im, 0, 0, $size, $size, $background); $font = __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; @@ -166,7 +173,7 @@ protected function generateAvatar(string $userDisplayName, int $size): string { $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); diff --git a/lib/private/Avatar/UserAvatar.php b/lib/private/Avatar/UserAvatar.php index f5a1d7e77b177..02fcfcb0fc8ca 100644 --- a/lib/private/Avatar/UserAvatar.php +++ b/lib/private/Avatar/UserAvatar.php @@ -208,7 +208,14 @@ public function remove(bool $silent = false): void { * * @throws NotFoundException */ - private function getExtension(): string { + private function getExtension(bool $generated, bool $darkTheme): string { + if ($darkTheme && !$generated) { + if ($this->folder->fileExists('avatar-dark.jpg')) { + return 'jpg'; + } elseif ($this->folder->fileExists('avatar-dark.png')) { + return 'png'; + } + } if ($this->folder->fileExists('avatar.jpg')) { return 'jpg'; } elseif ($this->folder->fileExists('avatar.png')) { @@ -228,25 +235,36 @@ private function getExtension(): string { * @throws \OCP\Files\NotPermittedException * @throws \OCP\PreConditionNotMetException */ - public function getFile(int $size): ISimpleFile { + public function getFile(int $size, bool $darkTheme = false): ISimpleFile { + $generated = $this->folder->fileExists('generated'); + try { - $ext = $this->getExtension(); + $ext = $this->getExtension($generated, $darkTheme); } catch (NotFoundException $e) { - if (!$data = $this->generateAvatarFromSvg(1024)) { - $data = $this->generateAvatar($this->getDisplayName(), 1024); + if (!$data = $this->generateAvatarFromSvg(1024, $darkTheme)) { + $data = $this->generateAvatar($this->getDisplayName(), 1024, $darkTheme); } - $avatar = $this->folder->newFile('avatar.png'); + $avatar = $this->folder->newFile($darkTheme ? 'avatar-dark.png' : 'avatar.png'); $avatar->putContent($data); $ext = 'png'; $this->folder->newFile('generated', ''); $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true'); + $generated = true; } - if ($size === -1) { - $path = 'avatar.' . $ext; + if ($generated) { + if ($size === -1) { + $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $ext; + } else { + $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $size . '.' . $ext; + } } else { - $path = 'avatar.' . $size . '.' . $ext; + if ($size === -1) { + $path = 'avatar.' . $ext; + } else { + $path = 'avatar.' . $size . '.' . $ext; + } } try { @@ -255,11 +273,9 @@ public function getFile(int $size): ISimpleFile { if ($size <= 0) { throw new NotFoundException; } - - // TODO: rework to integrate with the PlaceholderAvatar in a compatible way - if ($this->folder->fileExists('generated')) { - if (!$data = $this->generateAvatarFromSvg($size)) { - $data = $this->generateAvatar($this->getDisplayName(), $size); + if ($generated) { + if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) { + $data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme); } } else { $avatar = new \OCP\Image(); @@ -279,7 +295,7 @@ public function getFile(int $size): ISimpleFile { } if ($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) { - $generated = $this->folder->fileExists('generated') ? 'true' : 'false'; + $generated = $generated ? 'true' : 'false'; $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', $generated); } diff --git a/lib/public/Color.php b/lib/public/Color.php index e2cabd9c2fc62..6c9a35ca49c2a 100644 --- a/lib/public/Color.php +++ b/lib/public/Color.php @@ -125,6 +125,14 @@ public static function mixPalette(int $steps, Color $color1, Color $color2): arr return $palette; } + public function alphaBlending(float $opacity, Color $source): Color { + return new Color( + (int)((1 - $opacity) * $source->red() + $opacity * $this->red()), + (int)((1 - $opacity) * $source->green() + $opacity * $this->green()), + (int)((1 - $opacity) * $source->blue() + $opacity * $this->blue()) + ); + } + /** * Calculate steps between two Colors * @param int $steps start color diff --git a/lib/public/IAvatar.php b/lib/public/IAvatar.php index d05a12e1dbf4b..f9fe9a645e65b 100644 --- a/lib/public/IAvatar.php +++ b/lib/public/IAvatar.php @@ -39,10 +39,11 @@ interface IAvatar { * Get the users avatar * * @param int $size size in px of the avatar, avatars are square, defaults to 64, -1 can be used to not scale the image + * @param bool $darkTheme Should the generated avatar be dark themed * @return false|\OCP\IImage containing the avatar or false if there's no image * @since 6.0.0 - size of -1 was added in 9.0.0 */ - public function get(int $size = 64); + public function get(int $size = 64, bool $darkTheme = false); /** * Check if an avatar exists for the user @@ -81,10 +82,11 @@ public function remove(bool $silent = false): void; * Get the file of the avatar * * @param int $size The desired image size. -1 can be used to not scale the image + * @param bool $darkTheme Should the generated avatar be dark themed * @throws NotFoundException * @since 9.0.0 */ - public function getFile(int $size): ISimpleFile; + public function getFile(int $size, bool $darkTheme = false): ISimpleFile; /** * Get the avatar background color