diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 11d81ab00b..6f0cf03d8e 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -41,6 +41,7 @@ use OCP\IL10N; use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; +use OCP\AppFramework\Http\DataResponse; /** * Class AvatarController @@ -113,6 +114,20 @@ class AvatarController extends Controller { + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @NoSameSiteCookieRequired + * @PublicPage + * + * Shortcut to getAvatar + */ + public function getAvatarPng($userId, $size) { + return $this->getAvatar($userId, $size, true); + } + + /** * @NoAdminRequired * @NoCSRFRequired @@ -121,24 +136,37 @@ class AvatarController extends Controller { * * @param string $userId * @param int $size + * @param bool $png return png or not * @return JSONResponse|FileDisplayResponse */ - public function getAvatar($userId, $size) { + public function getAvatar($userId, $size, bool $png = false) { if ($size > 2048) { $size = 2048; } elseif ($size <= 0) { $size = 64; } - try { - $avatar = $this->avatarManager->getAvatar($userId)->getFile($size); - $resp = new FileDisplayResponse($avatar, + if ($png === false) { + $avatar = $this->avatarManager->getAvatar($userId)->getAvatarVector($size); + $resp = new DataDisplayResponse( + $avatar, Http::STATUS_OK, - ['Content-Type' => $avatar->getMimeType()]); - } catch (\Exception $e) { - $resp = new Http\Response(); - $resp->setStatus(Http::STATUS_NOT_FOUND); - return $resp; + ['Content-Type' => 'image/svg+xml' + ]); + } else { + + try { + $avatar = $this->avatarManager->getAvatar($userId)->getFile($size); + $resp = new FileDisplayResponse( + $avatar, + Http::STATUS_OK, + ['Content-Type' => $avatar->getMimeType() + ]); + } catch (\Exception $e) { + $resp = new Http\Response(); + $resp->setStatus(Http::STATUS_NOT_FOUND); + return $resp; + } } // Cache for 30 minutes diff --git a/core/routes.php b/core/routes.php index cc1bd34d89..dd35638a7e 100644 --- a/core/routes.php +++ b/core/routes.php @@ -42,6 +42,7 @@ $application->registerRoutes($this, [ ['name' => 'lost#setPassword', 'url' => '/lostpassword/set/{token}/{userId}', 'verb' => 'POST'], ['name' => 'user#getDisplayNames', 'url' => '/displaynames', 'verb' => 'POST'], ['name' => 'avatar#getAvatar', 'url' => '/avatar/{userId}/{size}', 'verb' => 'GET'], + ['name' => 'avatar#getAvatarPng', 'url' => '/avatar/{userId}/{size}/png', 'verb' => 'GET'], ['name' => 'avatar#deleteAvatar', 'url' => '/avatar/', 'verb' => 'DELETE'], ['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'], ['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'], diff --git a/lib/private/Avatar.php b/lib/private/Avatar.php index 9524b36f8e..07e8f2522c 100644 --- a/lib/private/Avatar.php +++ b/lib/private/Avatar.php @@ -30,7 +30,6 @@ namespace OC; -use OC\User\User; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; @@ -39,8 +38,9 @@ use OCP\IAvatar; use OCP\IConfig; use OCP\IImage; use OCP\IL10N; -use OC_Image; use OCP\ILogger; +use OC\User\User; +use OC_Image; /** * This class gets and sets users avatars. @@ -57,6 +57,12 @@ class Avatar implements IAvatar { private $logger; /** @var IConfig */ private $config; + /** @var string */ + private $svgTemplate = ' + + + {letter} + '; /** * constructor @@ -68,10 +74,10 @@ class Avatar implements IAvatar { * @param IConfig $config */ public function __construct(ISimpleFolder $folder, - IL10N $l, - $user, - ILogger $logger, - IConfig $config) { + IL10N $l, + $user, + ILogger $logger, + IConfig $config) { $this->folder = $folder; $this->l = $l; $this->user = $user; @@ -82,7 +88,7 @@ class Avatar implements IAvatar { /** * @inheritdoc */ - public function get ($size = 64) { + public function get($size = 64) { try { $file = $this->getFile($size); } catch (NotFoundException $e) { @@ -111,17 +117,17 @@ class Avatar implements IAvatar { * @throws \Exception if the provided image is not valid * @throws NotSquareException if the image is not square * @return void - */ - public function set ($data) { + */ + public function set($data) { - if($data instanceOf IImage) { + if ($data instanceof IImage) { $img = $data; $data = $img->data(); } else { $img = new OC_Image(); if (is_resource($data) && get_resource_type($data) === "gd") { $img->setResource($data); - } elseif(is_resource($data)) { + } elseif (is_resource($data)) { $img->loadFromFileHandle($data); } else { try { @@ -154,7 +160,7 @@ class Avatar implements IAvatar { } $this->remove(); - $file = $this->folder->newFile('avatar.'.$type); + $file = $this->folder->newFile('avatar.' . $type); $file->putContent($data); try { @@ -165,17 +171,17 @@ class Avatar implements IAvatar { // } $this->user->triggerChange('avatar', $file); - } + } /** * remove the users avatar * @return void - */ - public function remove () { + */ + public function remove() { $avatars = $this->folder->getDirectoryListing(); $this->config->setUserValue($this->user->getUID(), 'avatar', 'version', - (int)$this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1); + (int) $this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1); foreach ($avatars as $avatar) { $avatar->delete(); @@ -235,7 +241,7 @@ class Avatar implements IAvatar { } - if($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) { + if ($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) { $generated = $this->folder->fileExists('generated') ? 'true' : 'false'; $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', $generated); } @@ -257,6 +263,35 @@ class Avatar implements IAvatar { } throw new NotFoundException; } + + /** + * https://github.com/sebdesign/cap-height -- for 500px height + * Open Sans cap-height is 0.72 and we want a 200px caps height size (0.4 letter-to-total-height ratio, 500*0.4=200). 200/0.72 = 278px. + * 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 --> + * {size} = 500 + * {fill} = hex color to fill + * {y} = top to bottom baseline text-anchor y position + * {font} = font size + * {letter} = Letter to display + * + * Generate SVG avatar + * @return string + * + */ + public function getAvatarVector($size) { + $userDisplayName = $this->user->getDisplayName(); + + $bgRGB = $this->avatarBackgroundColor($userDisplayName); + $bgHEX = sprintf("%02x%02x%02x", $bgRGB->r, $bgRGB->g, $bgRGB->b); + + $letter = mb_strtoupper(mb_substr($userDisplayName, 0, 1), 'UTF-8'); + $font = round($size * 0.4); + $fontSize = round($font / 0.72); + $y = round($size/2 + $font/2); + $toReplace = ['{size}', '{fill}', '{y}', '{font}', '{letter}']; + + return str_replace($toReplace, [$size, $bgHEX, $y, $fontSize, $letter], $this->svgTemplate); + } /** * @param string $userDisplayName @@ -295,10 +330,10 @@ class Avatar implements IAvatar { * @param string $text text string * @param string $font font path * @param int $size font size - * @param int $angle + * @param int $angle * @return Array */ - protected function imageTTFCenter($image, string $text, string $font, int $size, $angle = 0): Array { + protected function imageTTFCenter($image, string $text, string $font, int $size, $angle = 0): array { // Image width & height $xi = imagesx($image); $yi = imagesy($image); @@ -330,6 +365,7 @@ class Avatar implements IAvatar { $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 @@ -344,12 +380,11 @@ class Avatar implements IAvatar { $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); + $palette[] = new Color($r, $g, $b); } return $palette; } - /** * Convert a string to an integer evenly * @param string $hash the text to parse @@ -361,7 +396,7 @@ class Avatar implements IAvatar { $result = array(); // Splitting evenly the string - for ($i=0; $i< strlen($hash); $i++) { + for ($i = 0; $i < strlen($hash); $i++) { // chars in md5 goes up to f, hex:16 $result[] = intval(substr($hash, $i, 1), 16) % 16; } @@ -373,12 +408,11 @@ class Avatar implements IAvatar { return intval($final % $maximum); } - /** * @param string $text * @return Color Object containting r g b int in the range [0, 255] */ - function avatarBackgroundColor($text) { + public function avatarBackgroundColor($text) { $hash = preg_replace('/[^0-9a-f]+/', '', $text); $hash = md5($hash); @@ -387,6 +421,7 @@ class Avatar implements IAvatar { $red = new Color(182, 70, 157); $yellow = new Color(221, 203, 85); $blue = new Color(0, 130, 201); // Nextcloud blue + // Number of steps to go from a color to another // 3 colors * 6 will result in 18 generated colors $steps = 6; @@ -397,7 +432,7 @@ class Avatar implements IAvatar { $finalPalette = array_merge($palette1, $palette2, $palette3); - return $finalPalette[$this->hashToInt($hash, $steps * 3 )]; + return $finalPalette[$this->hashToInt($hash, $steps * 3)]; } public function userChanged($feature, $oldValue, $newValue) {