-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix: generate favourite icon without imagick svg support #55132
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |||||
| use OCP\AppFramework\Http\NotFoundResponse; | ||||||
| use OCP\AppFramework\Http\Response; | ||||||
| use OCP\Files\NotFoundException; | ||||||
| use OCP\IConfig; | ||||||
| use OCP\IRequest; | ||||||
|
|
||||||
| class IconController extends Controller { | ||||||
|
|
@@ -30,6 +31,7 @@ class IconController extends Controller { | |||||
| public function __construct( | ||||||
| $appName, | ||||||
| IRequest $request, | ||||||
| private IConfig $config, | ||||||
| private ThemingDefaults $themingDefaults, | ||||||
| private IconBuilder $iconBuilder, | ||||||
| private ImageManager $imageManager, | ||||||
|
|
@@ -79,7 +81,7 @@ public function getThemedIcon(string $app, string $image): Response { | |||||
| * Return a 32x32 favicon as png | ||||||
| * | ||||||
| * @param string $app ID of the app | ||||||
| * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}> | ||||||
| * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}> | ||||||
| * @throws \Exception | ||||||
| * | ||||||
| * 200: Favicon returned | ||||||
|
|
@@ -95,12 +97,14 @@ public function getFavicon(string $app = 'core'): Response { | |||||
|
|
||||||
| $response = null; | ||||||
| $iconFile = null; | ||||||
| // retrieve instance favorite icon | ||||||
|
||||||
| // retrieve instance favorite icon | |
| // retrieve instance favicon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // retrieve or generate app specific favorite icon | |
| // retrieve or generate app specific favicon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest removing this comment unless there's additional context we should know. Right now it's just echoing the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to keep these in, they break up the code into specific logical section and tell us which icon is being worked with. User Instance icon, App specific icon or Generic core icon.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // fallback to core favorite icon | |
| // fallback to core favicon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
kesselb marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| namespace OCA\Theming; | ||
|
|
||
| use Imagick; | ||
| use ImagickDraw; | ||
| use ImagickPixel; | ||
| use OCP\Files\SimpleFS\ISimpleFile; | ||
|
|
||
|
|
@@ -30,17 +31,18 @@ public function __construct( | |
| * @return string|false image blob | ||
| */ | ||
| public function getFavicon($app) { | ||
| if (!$this->imageManager->shouldReplaceIcons()) { | ||
| if (!$this->imageManager->canConvert('PNG')) { | ||
| return false; | ||
| } | ||
| try { | ||
| $favicon = new Imagick(); | ||
| $favicon->setFormat('ico'); | ||
| $icon = $this->renderAppIcon($app, 128); | ||
| if ($icon === false) { | ||
| return false; | ||
| } | ||
| $icon->setImageFormat('png32'); | ||
| $icon->setImageFormat('PNG32'); | ||
|
|
||
| $favicon = new Imagick(); | ||
| $favicon->setFormat('ICO'); | ||
|
|
||
| $clone = clone $icon; | ||
| $clone->scaleImage(16, 0); | ||
|
|
@@ -96,7 +98,9 @@ public function getTouchIcon($app) { | |
| * @return Imagick|false | ||
| */ | ||
| public function renderAppIcon($app, $size) { | ||
| $appIcon = $this->util->getAppIcon($app); | ||
| $supportSvg = $this->imageManager->canConvert('SVG'); | ||
| // retrieve app icon | ||
| $appIcon = $this->util->getAppIcon($app, $supportSvg); | ||
| if ($appIcon instanceof ISimpleFile) { | ||
| $appIconContent = $appIcon->getContent(); | ||
| $mime = $appIcon->getMimeType(); | ||
|
|
@@ -111,79 +115,101 @@ public function renderAppIcon($app, $size) { | |
| return false; | ||
| } | ||
|
|
||
| $color = $this->themingDefaults->getColorPrimary(); | ||
| $appIconFile = null; | ||
kesselb marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $appIconIsSvg = ($mime === 'image/svg+xml' || substr($appIconContent, 0, 4) === '<svg'); | ||
kesselb marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // generate background image with rounded corners | ||
| $cornerRadius = 0.2 * $size; | ||
| $background = '<?xml version="1.0" encoding="UTF-8"?>' | ||
| . '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width="' . $size . '" height="' . $size . '" xmlns:xlink="http://www.w3.org/1999/xlink">' | ||
| . '<rect x="0" y="0" rx="' . $cornerRadius . '" ry="' . $cornerRadius . '" width="' . $size . '" height="' . $size . '" style="fill:' . $color . ';" />' | ||
| . '</svg>'; | ||
| // resize svg magic as this seems broken in Imagemagick | ||
| if ($mime === 'image/svg+xml' || substr($appIconContent, 0, 4) === '<svg') { | ||
| if (substr($appIconContent, 0, 5) !== '<?xml') { | ||
| $svg = '<?xml version="1.0"?>' . $appIconContent; | ||
| } else { | ||
| $svg = $appIconContent; | ||
| } | ||
| $tmp = new Imagick(); | ||
| $tmp->setBackgroundColor(new ImagickPixel('transparent')); | ||
| $tmp->setResolution(72, 72); | ||
| $tmp->readImageBlob($svg); | ||
| $x = $tmp->getImageWidth(); | ||
| $y = $tmp->getImageHeight(); | ||
| $tmp->destroy(); | ||
|
|
||
| // convert svg to resized image | ||
| // if source image is svg but svg not supported, abort | ||
kesselb marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ($appIconIsSvg && !$supportSvg) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| // construct original image object | ||
| $appIconFile = new Imagick(); | ||
| $resX = (int)(72 * $size / $x); | ||
| $resY = (int)(72 * $size / $y); | ||
| $appIconFile->setResolution($resX, $resY); | ||
| $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); | ||
| $appIconFile->readImageBlob($svg); | ||
|
|
||
| /** | ||
| * invert app icons for bright primary colors | ||
| * the default nextcloud logo will not be inverted to black | ||
| */ | ||
| if ($this->util->isBrightColor($color) | ||
| && !$appIcon instanceof ISimpleFile | ||
| && $app !== 'core' | ||
| ) { | ||
| $appIconFile->negateImage(false); | ||
|
|
||
| if ($appIconIsSvg) { | ||
| // handle SVG images | ||
| // ensure proper XML declaration | ||
| if (substr($appIconContent, 0, 5) !== '<?xml') { | ||
| $svg = '<?xml version="1.0"?>' . $appIconContent; | ||
| } else { | ||
| $svg = $appIconContent; | ||
| } | ||
| // get dimensions for resolution calculation | ||
| $tmp = new Imagick(); | ||
| $tmp->setBackgroundColor(new ImagickPixel('transparent')); | ||
| $tmp->setResolution(72, 72); | ||
| $tmp->readImageBlob($svg); | ||
| $x = $tmp->getImageWidth(); | ||
| $y = $tmp->getImageHeight(); | ||
| $tmp->destroy(); | ||
| // set resolution for proper scaling | ||
| $resX = (int)(72 * $size / $x); | ||
| $resY = (int)(72 * $size / $y); | ||
| $appIconFile->setResolution($resX, $resY); | ||
| $appIconFile->readImageBlob($svg); | ||
| } else { | ||
| // handle non-SVG images | ||
| $appIconFile->readImageBlob($appIconContent); | ||
| } | ||
| } else { | ||
| $appIconFile = new Imagick(); | ||
| $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); | ||
| $appIconFile->readImageBlob($appIconContent); | ||
| } catch (\ImagickException $e) { | ||
| return false; | ||
| } | ||
| // offset for icon positioning | ||
| $padding = 0.15; | ||
| $border_w = (int)($appIconFile->getImageWidth() * $padding); | ||
| $border_h = (int)($appIconFile->getImageHeight() * $padding); | ||
| $innerWidth = ($appIconFile->getImageWidth() - $border_w * 2); | ||
| $innerHeight = ($appIconFile->getImageHeight() - $border_h * 2); | ||
| $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight); | ||
| // center icon | ||
| $offset_w = (int)($size / 2 - $innerWidth / 2); | ||
| $offset_h = (int)($size / 2 - $innerHeight / 2); | ||
|
|
||
| $finalIconFile = new Imagick(); | ||
| $finalIconFile->setBackgroundColor(new ImagickPixel('transparent')); | ||
| $finalIconFile->readImageBlob($background); | ||
| $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT); | ||
| $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5'); | ||
| $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h); | ||
| $finalIconFile->setImageFormat('png24'); | ||
| if (defined('Imagick::INTERPOLATE_BICUBIC') === true) { | ||
| $filter = Imagick::INTERPOLATE_BICUBIC; | ||
| } else { | ||
| $filter = Imagick::FILTER_LANCZOS; | ||
| // calculate final image size and position | ||
| $padding = 0.85; | ||
| $original_w = $appIconFile->getImageWidth(); | ||
| $original_h = $appIconFile->getImageHeight(); | ||
| $contentSize = (int)floor($size * $padding); | ||
| $scale = min($contentSize / $original_w, $contentSize / $original_h); | ||
| $new_w = max(1, (int)floor($original_w * $scale)); | ||
| $new_h = max(1, (int)floor($original_h * $scale)); | ||
| $offset_w = (int)floor(($size - $new_w) / 2); | ||
| $offset_h = (int)floor(($size - $new_h) / 2); | ||
| $cornerRadius = 0.2 * $size; | ||
| $color = $this->themingDefaults->getColorPrimary(); | ||
| // resize original image | ||
| $appIconFile->resizeImage($new_w, $new_h, Imagick::FILTER_LANCZOS, 1); | ||
| /** | ||
| * invert app icons for bright primary colors | ||
| * the default nextcloud logo will not be inverted to black | ||
| */ | ||
| if ($this->util->isBrightColor($color) | ||
| && !$appIcon instanceof ISimpleFile | ||
| && $app !== 'core' | ||
| ) { | ||
| $appIconFile->negateImage(false); | ||
| } | ||
| // construct final image object | ||
| try { | ||
| // image background | ||
| $finalIconFile = new Imagick(); | ||
| $finalIconFile->setBackgroundColor(new ImagickPixel('transparent')); | ||
| // icon background | ||
| $finalIconFile->newImage($size, $size, new ImagickPixel('transparent')); | ||
| $draw = new ImagickDraw(); | ||
| $draw->setFillColor($color); | ||
| $draw->roundRectangle(0, 0, $size - 1, $size - 1, $cornerRadius, $cornerRadius); | ||
| $finalIconFile->drawImage($draw); | ||
| $draw->destroy(); | ||
| // overlay icon | ||
| $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT); | ||
| $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5'); | ||
| $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h); | ||
| $finalIconFile->setImageFormat('PNG32'); | ||
| if (defined('Imagick::INTERPOLATE_BICUBIC') === true) { | ||
| $filter = Imagick::INTERPOLATE_BICUBIC; | ||
| } else { | ||
| $filter = Imagick::FILTER_LANCZOS; | ||
| } | ||
| $finalIconFile->resizeImage($size, $size, $filter, 1, false); | ||
|
|
||
| return $finalIconFile; | ||
| } finally { | ||
| unset($appIconFile); | ||
| } | ||
| $finalIconFile->resizeImage($size, $size, $filter, 1, false); | ||
|
|
||
| $appIconFile->destroy(); | ||
| return $finalIconFile; | ||
| return false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you forgot the catch block for the exception?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, this is just a default return, this should never get triggered but I did not want to change the function signature. The original code did not throw a error, so just matching that.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The try-catch-finally block does not catch any exceptions. That means if an error occurs, the variable is unset and the exception still bubbles up. If no exception occurs, the function returns earlier. This makes the final return block unreachable. It is not documented that the functions throw and the previous try-catch block does catch the ImagickException, so I wanted to check whether the catch block is missing. |
||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code already shows that we retrieve the icon. A comment would be more useful if it explained the reasoning or any non-obvious behavior.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does explain what we are doing? We are retrieving the user instance configured favorite icon.