Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avatar new style #33752

Merged
merged 4 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
CarlSchwan marked this conversation as resolved.
Show resolved Hide resolved
$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