Skip to content

Commit

Permalink
Merge pull request #33752 from nextcloud/avatar-new-style
Browse files Browse the repository at this point in the history
Avatar new style
  • Loading branch information
CarlSchwan authored Sep 9, 2022
2 parents 76f42e1 + bc9a488 commit 4a82396
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 67 deletions.
1 change: 0 additions & 1 deletion apps/user_status/src/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const propsData = {
},
user: avatarDiv.dataset.user,
displayName: avatarDiv.dataset.displayname,
url: avatarDiv.dataset.avatar,
disableMenu: true,
disableTooltip: true,
}
Expand Down
38 changes: 38 additions & 0 deletions core/Controller/AvatarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions core/Controller/GuestAvatarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ public function __construct(
* @param string $size The desired avatar size, e.g. 64 for 64x64px
* @return FileDisplayResponse|Http\Response
*/
public function getAvatar(string $guestName, string $size) {
public function getAvatar(string $guestName, string $size, ?bool $darkTheme = false) {
$size = (int) $size;
$darkTheme = $darkTheme ?? false;

if ($size <= 64) {
if ($size !== 64) {
Expand All @@ -77,7 +78,7 @@ public function getAvatar(string $guestName, string $size) {

try {
$avatar = $this->avatarManager->getGuestAvatar($guestName);
$avatarFile = $avatar->getFile($size);
$avatarFile = $avatar->getFile($size, $darkTheme);

$resp = new FileDisplayResponse(
$avatarFile,
Expand All @@ -94,7 +95,15 @@ public function getAvatar(string $guestName, string $size) {
}

// Cache for 30 minutes
$resp->cacheFor(1800);
$resp->cacheFor(1800, false, true);
return $resp;
}

/**
* @PublicPage
* @NoCSRFRequired
*/
public function getAvatarDark(string $guestName, string $size) {
return $this->getAvatar($guestName, $size, true);
}
}
2 changes: 2 additions & 0 deletions core/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
['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'],
['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'],
['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'],
['name' => 'GuestAvatar#getAvatarDark', 'url' => '/avatar/guest/{guestName}/{size}/dark', 'verb' => 'GET'],
['name' => 'GuestAvatar#getAvatar', 'url' => '/avatar/guest/{guestName}/{size}', 'verb' => 'GET'],
['name' => 'CSRFToken#index', 'url' => '/csrftoken', 'verb' => 'GET'],
['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'],
Expand Down
4 changes: 2 additions & 2 deletions dist/user_status-menu.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/user_status-menu.js.map

Large diffs are not rendered by default.

37 changes: 22 additions & 15 deletions lib/private/Avatar/Avatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ abstract class Avatar implements IAvatar {
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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -111,25 +111,27 @@ public function get(int $size = 64) {
* @return string
*
*/
protected function getAvatarVector(int $size): string {
protected function getAvatarVector(int $size, bool $darkTheme): 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, $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);
}

/**
* Generate png avatar from svg with Imagick
*/
protected function generateAvatarFromSvg(int $size): ?string {
protected function generateAvatarFromSvg(int $size, bool $darkTheme): ?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, $darkTheme);
$avatar = new Imagick();
$avatar->setFont($font);
$avatar->readImageBlob($svg);
Expand All @@ -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 $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);
$background = imagecolorallocate(
Expand All @@ -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';
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions lib/private/Avatar/GuestAvatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ public function remove(bool $silent = false): void {
/**
* Generates an avatar for the guest.
*/
public function getFile(int $size): ISimpleFile {
$avatar = $this->generateAvatar($this->userDisplayName, $size);
public function getFile(int $size, bool $darkTheme = false): ISimpleFile {
$avatar = $this->generateAvatar($this->userDisplayName, $size, $darkTheme);
return new InMemoryFile('avatar.png', $avatar);
}

Expand Down
10 changes: 5 additions & 5 deletions lib/private/Avatar/PlaceholderAvatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ public function remove(bool $silent = false): void {
* @throws \OCP\Files\NotPermittedException
* @throws \OCP\PreConditionNotMetException
*/
public function getFile(int $size): ISimpleFile {
public function getFile(int $size, bool $darkTheme = false): ISimpleFile {
$ext = 'png';

if ($size === -1) {
$path = 'avatar-placeholder.' . $ext;
$path = 'avatar-placeholder' . ($darkTheme ? '-dark' : '') . '.' . $ext;
} else {
$path = 'avatar-placeholder.' . $size . '.' . $ext;
$path = 'avatar-placeholder' . ($darkTheme ? '-dark' : '') . '.' . $size . '.' . $ext;
}

try {
Expand All @@ -124,8 +124,8 @@ public function getFile(int $size): ISimpleFile {
throw new NotFoundException;
}

if (!$data = $this->generateAvatarFromSvg($size)) {
$data = $this->generateAvatar($this->getDisplayName(), $size);
if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) {
$data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme);
}

try {
Expand Down
46 changes: 31 additions & 15 deletions lib/private/Avatar/UserAvatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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);
}

Expand Down
20 changes: 7 additions & 13 deletions lib/private/Repair/ClearGeneratedAvatarCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,29 @@
use OCP\Migration\IRepairStep;

class ClearGeneratedAvatarCache implements IRepairStep {

/** @var AvatarManager */
protected $avatarManager;

/** @var IConfig */
private $config;
protected AvatarManager $avatarManager;
private IConfig $config;

public function __construct(IConfig $config, AvatarManager $avatarManager) {
$this->config = $config;
$this->avatarManager = $avatarManager;
}

public function getName() {
public function getName(): string {
return 'Clear every generated avatar on major updates';
}

/**
* Check if this repair step should run
*
* @return boolean
*/
private function shouldRun() {
private function shouldRun(): bool {
$versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0');

// was added to 15.0.0.4
return version_compare($versionFromBeforeUpdate, '15.0.0.4', '<=');
// was added to 25.0.0.10
return version_compare($versionFromBeforeUpdate, '25.0.0.10', '<=');
}

public function run(IOutput $output) {
public function run(IOutput $output): void {
if ($this->shouldRun()) {
try {
$this->avatarManager->clearCachedAvatars();
Expand Down
14 changes: 14 additions & 0 deletions lib/public/Color.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ public static function mixPalette(int $steps, Color $color1, Color $color2): arr
return $palette;
}

/**
* Alpha blend another color with a given opacity to this color
*
* @return Color The new color
* @since 25.0.0
*/
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
Expand Down
Loading

0 comments on commit 4a82396

Please sign in to comment.