From e2c4db1541c60060846a79bba4246e172bf57a84 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Mon, 15 Sep 2025 17:37:47 -0400 Subject: [PATCH] fix: generate favourite icon without imagick svg support Signed-off-by: SebastianKrupinski --- .../theming/lib/Controller/IconController.php | 20 +- .../lib/Controller/ThemingController.php | 8 +- apps/theming/lib/IconBuilder.php | 166 +++++---- apps/theming/lib/ImageManager.php | 44 ++- apps/theming/lib/ThemingDefaults.php | 4 +- apps/theming/lib/Util.php | 20 +- apps/theming/openapi.json | 8 +- .../tests/Controller/IconControllerTest.php | 32 +- .../Controller/ThemingControllerTest.php | 4 +- apps/theming/tests/IconBuilderTest.php | 336 ++++++++++++++---- apps/theming/tests/data/favicon-original.ico | Bin 1618 -> 0 bytes apps/theming/tests/data/logo.png | Bin 0 -> 3571 bytes apps/theming/tests/data/logo.png.license | 2 + apps/theming/tests/data/logo.svg | 1 + apps/theming/tests/data/logo.svg.license | 2 + apps/theming/tests/data/settings.png | Bin 0 -> 3082 bytes apps/theming/tests/data/settings.png.license | 2 + apps/theming/tests/data/settings.svg | 1 + apps/theming/tests/data/settings.svg.license | 2 + apps/theming/tests/data/touch-comments.png | Bin 7145 -> 0 bytes apps/theming/tests/data/touch-core-red.png | Bin 8278 -> 0 bytes .../theming/tests/data/touch-original-png.png | Bin 10235 -> 0 bytes apps/theming/tests/data/touch-original.png | Bin 8115 -> 0 bytes apps/theming/tests/data/touch-testing-red.png | Bin 8550 -> 0 bytes openapi.json | 8 +- 25 files changed, 474 insertions(+), 186 deletions(-) delete mode 100644 apps/theming/tests/data/favicon-original.ico create mode 100644 apps/theming/tests/data/logo.png create mode 100644 apps/theming/tests/data/logo.png.license create mode 100644 apps/theming/tests/data/logo.svg create mode 100644 apps/theming/tests/data/logo.svg.license create mode 100644 apps/theming/tests/data/settings.png create mode 100644 apps/theming/tests/data/settings.png.license create mode 100644 apps/theming/tests/data/settings.svg create mode 100644 apps/theming/tests/data/settings.svg.license delete mode 100644 apps/theming/tests/data/touch-comments.png delete mode 100644 apps/theming/tests/data/touch-core-red.png delete mode 100644 apps/theming/tests/data/touch-original-png.png delete mode 100644 apps/theming/tests/data/touch-original.png delete mode 100644 apps/theming/tests/data/touch-testing-red.png diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php index e82faf78a79..ed4026944bd 100644 --- a/apps/theming/lib/Controller/IconController.php +++ b/apps/theming/lib/Controller/IconController.php @@ -21,6 +21,7 @@ use OCP\AppFramework\Http\FileDisplayResponse; 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 @@ class IconController extends Controller { * Return a 32x32 favicon as png * * @param string $app ID of the app - * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse + * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse * @throws \Exception * * 200: Favicon returned @@ -95,12 +97,14 @@ class IconController extends Controller { $response = null; $iconFile = null; + // retrieve instance favicon try { $iconFile = $this->imageManager->getImage('favicon', false); $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); } catch (NotFoundException $e) { } - if ($iconFile === null && $this->imageManager->shouldReplaceIcons()) { + // retrieve or generate app specific favicon + if (($this->imageManager->canConvert('PNG') || $this->imageManager->canConvert('SVG')) && $this->imageManager->canConvert('ICO')) { $color = $this->themingDefaults->getColorPrimary(); try { $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color); @@ -113,9 +117,10 @@ class IconController extends Controller { } $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); } + // fallback to core favicon if ($response === null) { $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png'; - $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']); } $response->cacheFor(86400); return $response; @@ -125,7 +130,7 @@ class IconController extends Controller { * Return a 512x512 icon for touch devices * * @param string $app ID of the app - * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse + * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse * @throws \Exception * * 200: Touch icon returned @@ -140,12 +145,14 @@ class IconController extends Controller { } $response = null; + // retrieve instance favicon try { $iconFile = $this->imageManager->getImage('favicon'); - $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => $iconFile->getMimeType()]); } catch (NotFoundException $e) { } - if ($this->imageManager->shouldReplaceIcons()) { + // retrieve or generate app specific touch icon + if ($this->imageManager->canConvert('PNG')) { $color = $this->themingDefaults->getColorPrimary(); try { $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color); @@ -158,6 +165,7 @@ class IconController extends Controller { } $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']); } + // fallback to core touch icon if ($response === null) { $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png'; $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']); diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index d5b0cbd15a3..ff8012c4ecd 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -366,6 +366,7 @@ class ThemingController extends Controller { #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getImage(string $key, bool $useSvg = true) { try { + $useSvg = $useSvg && $this->imageManager->canConvert('SVG'); $file = $this->imageManager->getImage($key, $useSvg); } catch (NotFoundException $e) { return new NotFoundResponse(); @@ -376,13 +377,8 @@ class ThemingController extends Controller { $csp->allowInlineStyle(); $response->setContentSecurityPolicy($csp); $response->cacheFor(3600); - $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', '')); + $response->addHeader('Content-Type', $file->getMimeType()); $response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"'); - if (!$useSvg) { - $response->addHeader('Content-Type', 'image/png'); - } else { - $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', '')); - } return $response; } diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php index 25dedccc6f5..b745815ae9b 100644 --- a/apps/theming/lib/IconBuilder.php +++ b/apps/theming/lib/IconBuilder.php @@ -7,6 +7,7 @@ namespace OCA\Theming; use Imagick; +use ImagickDraw; use ImagickPixel; use OCP\Files\SimpleFS\ISimpleFile; @@ -30,17 +31,18 @@ class IconBuilder { * @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 @@ class IconBuilder { * @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,78 +115,100 @@ class IconBuilder { return false; } - $color = $this->themingDefaults->getColorPrimary(); + $appIconIsSvg = ($mime === 'image/svg+xml' || str_starts_with($appIconContent, '' - . '' - . ''; - // resize svg magic as this seems broken in Imagemagick - if ($mime === 'image/svg+xml' || substr($appIconContent, 0, 4) === 'setBackgroundColor(new ImagickPixel('transparent')); + + if ($appIconIsSvg) { + // handle SVG images + // ensure proper XML declaration + if (!str_starts_with($appIconContent, '' . $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 { - $svg = $appIconContent; + // handle non-SVG images + $appIconFile->readImageBlob($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 - $appIconFile = new Imagick(); - $res = (int)(72 * $size / max($x, $y)); - $appIconFile->setResolution($res, $res); - $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); + } catch (\ImagickException $e) { + return false; + } + // 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; } - } else { - $appIconFile = new Imagick(); - $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); - $appIconFile->readImageBlob($appIconContent); - } - // 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->resizeImage($size, $size, $filter, 1, false); - $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; + return $finalIconFile; + } finally { + unset($appIconFile); } - $finalIconFile->resizeImage($size, $size, $filter, 1, false); - $appIconFile->destroy(); - return $finalIconFile; + return false; } /** diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php index 309bf192bc3..c25cf5bdade 100644 --- a/apps/theming/lib/ImageManager.php +++ b/apps/theming/lib/ImageManager.php @@ -85,18 +85,37 @@ class ImageManager { public function getImage(string $key, bool $useSvg = true): ISimpleFile { $mime = $this->config->getAppValue('theming', $key . 'Mime', ''); $folder = $this->getRootFolder()->getFolder('images'); + $useSvg = $useSvg && $this->canConvert('SVG'); if ($mime === '' || !$folder->fileExists($key)) { throw new NotFoundException(); } - - if (!$useSvg && $this->shouldReplaceIcons()) { + // if SVG was requested and is supported + if ($useSvg) { + if (!$folder->fileExists($key . '.svg')) { + try { + $finalIconFile = new \Imagick(); + $finalIconFile->setBackgroundColor('none'); + $finalIconFile->readImageBlob($folder->getFile($key)->getContent()); + $finalIconFile->setImageFormat('SVG'); + $svgFile = $folder->newFile($key . '.svg'); + $svgFile->putContent($finalIconFile->getImageBlob()); + return $svgFile; + } catch (\ImagickException $e) { + $this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: ' . $e->getMessage()); + } + } else { + return $folder->getFile($key . '.svg'); + } + } + // if SVG was not requested, but PNG is supported + if (!$useSvg && $this->canConvert('PNG')) { if (!$folder->fileExists($key . '.png')) { try { $finalIconFile = new \Imagick(); $finalIconFile->setBackgroundColor('none'); $finalIconFile->readImageBlob($folder->getFile($key)->getContent()); - $finalIconFile->setImageFormat('png32'); + $finalIconFile->setImageFormat('PNG32'); $pngFile = $folder->newFile($key . '.png'); $pngFile->putContent($finalIconFile->getImageBlob()); return $pngFile; @@ -107,7 +126,7 @@ class ImageManager { return $folder->getFile($key . '.png'); } } - + // fallback to the original file return $folder->getFile($key); } @@ -328,7 +347,7 @@ class ImageManager { public function getSupportedUploadImageFormats(string $key): array { $supportedFormats = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if ($key !== 'favicon' || $this->shouldReplaceIcons() === true) { + if ($key !== 'favicon' || $this->canConvert('SVG') === true) { $supportedFormats[] = 'image/svg+xml'; $supportedFormats[] = 'image/svg'; } @@ -364,17 +383,26 @@ class ImageManager { * @return bool */ public function shouldReplaceIcons() { + return $this->canConvert('SVG'); + } + + /** + * Check if Imagemagick is enabled and if format is supported + * + * @return bool + */ + public function canConvert(string $format): bool { $cache = $this->cacheFactory->createDistributed('theming-' . $this->urlGenerator->getBaseUrl()); - if ($value = $cache->get('shouldReplaceIcons')) { + if ($value = $cache->get('convert-' . $format)) { return (bool)$value; } $value = false; if (extension_loaded('imagick')) { - if (count(\Imagick::queryFormats('SVG')) >= 1) { + if (count(\Imagick::queryFormats($format)) >= 1) { $value = true; } } - $cache->set('shouldReplaceIcons', $value); + $cache->set('convert-' . $format, $value); return $value; } diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index 445ab33e256..c5b211a5b19 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -379,10 +379,10 @@ class ThemingDefaults extends \OC_Defaults { } $route = false; - if ($image === 'favicon.ico' && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) { + if ($image === 'favicon.ico' && ($this->imageManager->canConvert('ICO') || $this->getCustomFavicon() !== null)) { $route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]); } - if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) { + if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->canConvert('PNG') || $this->getCustomFavicon() !== null)) { $route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]); } if ($image === 'manifest.json') { diff --git a/apps/theming/lib/Util.php b/apps/theming/lib/Util.php index 385fc14fac4..a3a9c3240a8 100644 --- a/apps/theming/lib/Util.php +++ b/apps/theming/lib/Util.php @@ -206,30 +206,38 @@ class Util { * @param string $app app name * @return string|ISimpleFile path to app icon / file of logo */ - public function getAppIcon($app) { + public function getAppIcon($app, $useSvg = true) { $app = $this->appManager->cleanAppId($app); try { + // find app specific icon $appPath = $this->appManager->getAppPath($app); - $icon = $appPath . '/img/' . $app . '.svg'; + $extension = ($useSvg ? '.svg' : '.png'); + + $icon = $appPath . '/img/' . $app . $extension; if (file_exists($icon)) { return $icon; } - $icon = $appPath . '/img/app.svg'; + + $icon = $appPath . '/img/app' . $extension; if (file_exists($icon)) { return $icon; } } catch (AppPathNotFoundException $e) { } - + // fallback to custom instance logo if ($this->config->getAppValue('theming', 'logoMime', '') !== '') { - $logoFile = null; try { $folder = $this->appData->getFolder('global/images'); return $folder->getFile('logo'); } catch (NotFoundException $e) { } } - return \OC::$SERVERROOT . '/core/img/logo/logo.svg'; + // fallback to core logo + if ($useSvg) { + return \OC::$SERVERROOT . '/core/img/logo/logo.svg'; + } else { + return \OC::$SERVERROOT . '/core/img/logo/logo.png'; + } } /** diff --git a/apps/theming/openapi.json b/apps/theming/openapi.json index 33f9c54cc27..0ab33e2cd81 100644 --- a/apps/theming/openapi.json +++ b/apps/theming/openapi.json @@ -437,6 +437,12 @@ "200": { "description": "Favicon returned", "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + }, "image/x-icon": { "schema": { "type": "string", @@ -506,7 +512,7 @@ "format": "binary" } }, - "image/x-icon": { + "*/*": { "schema": { "type": "string", "format": "binary" diff --git a/apps/theming/tests/Controller/IconControllerTest.php b/apps/theming/tests/Controller/IconControllerTest.php index 018947d4a67..ebc392fc0bb 100644 --- a/apps/theming/tests/Controller/IconControllerTest.php +++ b/apps/theming/tests/Controller/IconControllerTest.php @@ -20,6 +20,7 @@ use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\File; use OCP\Files\NotFoundException; +use OCP\IConfig; use OCP\IRequest; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -33,6 +34,7 @@ class IconControllerTest extends TestCase { private IAppManager&MockObject $appManager; private ImageManager&MockObject $imageManager; private IconController $iconController; + private IConfig&MockObject $config; protected function setUp(): void { $this->request = $this->createMock(IRequest::class); @@ -41,6 +43,7 @@ class IconControllerTest extends TestCase { $this->imageManager = $this->createMock(ImageManager::class); $this->fileAccessHelper = $this->createMock(FileAccessHelper::class); $this->appManager = $this->createMock(IAppManager::class); + $this->config = $this->createMock(IConfig::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->timeFactory->expects($this->any()) @@ -52,6 +55,7 @@ class IconControllerTest extends TestCase { $this->iconController = new IconController( 'theming', $this->request, + $this->config, $this->themingDefaults, $this->iconBuilder, $this->imageManager, @@ -84,7 +88,7 @@ class IconControllerTest extends TestCase { $this->assertEquals($expected, $this->iconController->getThemedIcon('core', 'filetypes/folder.svg')); } - public function testGetFaviconDefault(): void { + public function testGetFaviconThemed(): void { if (!extension_loaded('imagick')) { $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); } @@ -98,8 +102,12 @@ class IconControllerTest extends TestCase { ->with('favicon') ->willThrowException(new NotFoundException()); $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(true); + ->method('canConvert') + ->willReturnMap([ + ['SVG', true], + ['PNG', true], + ['ICO', true], + ]); $this->imageManager->expects($this->once()) ->method('getCachedImage') ->willThrowException(new NotFoundException()); @@ -116,20 +124,24 @@ class IconControllerTest extends TestCase { $this->assertEquals($expected, $this->iconController->getFavicon()); } - public function testGetFaviconFail(): void { + public function testGetFaviconDefault(): void { $this->imageManager->expects($this->once()) ->method('getImage') ->with('favicon', false) ->willThrowException(new NotFoundException()); $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(false); + ->method('canConvert') + ->willReturnMap([ + ['SVG', false], + ['PNG', false], + ['ICO', false], + ]); $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png'; $this->fileAccessHelper->expects($this->once()) ->method('file_get_contents') ->with($fallbackLogo) ->willReturn(file_get_contents($fallbackLogo)); - $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']); $expected->cacheFor(86400); $this->assertEquals($expected, $this->iconController->getFavicon()); } @@ -147,7 +159,8 @@ class IconControllerTest extends TestCase { ->method('getImage') ->willThrowException(new NotFoundException()); $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') + ->method('canConvert') + ->with('PNG') ->willReturn(true); $this->iconBuilder->expects($this->once()) ->method('getTouchIcon') @@ -172,7 +185,8 @@ class IconControllerTest extends TestCase { ->with('favicon') ->willThrowException(new NotFoundException()); $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') + ->method('canConvert') + ->with('PNG') ->willReturn(false); $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png'; $this->fileAccessHelper->expects($this->once()) diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index 5925404b4ea..6f734b63510 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -648,6 +648,7 @@ class ThemingControllerTest extends TestCase { $file = $this->createMock(ISimpleFile::class); $file->method('getName')->willReturn('logo.svg'); $file->method('getMTime')->willReturn(42); + $file->method('getMimeType')->willReturn('text/svg'); $this->imageManager->expects($this->once()) ->method('getImage') ->willReturn($file); @@ -664,7 +665,7 @@ class ThemingControllerTest extends TestCase { $csp = new ContentSecurityPolicy(); $csp->allowInlineStyle(); $expected->setContentSecurityPolicy($csp); - @$this->assertEquals($expected, $this->themingController->getImage('logo')); + @$this->assertEquals($expected, $this->themingController->getImage('logo', true)); } @@ -680,6 +681,7 @@ class ThemingControllerTest extends TestCase { $file = $this->createMock(ISimpleFile::class); $file->method('getName')->willReturn('background.png'); $file->method('getMTime')->willReturn(42); + $file->method('getMimeType')->willReturn('image/png'); $this->imageManager->expects($this->once()) ->method('getImage') ->willReturn($file); diff --git a/apps/theming/tests/IconBuilderTest.php b/apps/theming/tests/IconBuilderTest.php index ac3de267c7f..ee7103691b5 100644 --- a/apps/theming/tests/IconBuilderTest.php +++ b/apps/theming/tests/IconBuilderTest.php @@ -13,9 +13,7 @@ use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; use OCP\App\IAppManager; -use OCP\Files\NotFoundException; use OCP\IConfig; -use OCP\ServerVersion; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -25,7 +23,7 @@ class IconBuilderTest extends TestCase { protected ThemingDefaults&MockObject $themingDefaults; protected ImageManager&MockObject $imageManager; protected IAppManager&MockObject $appManager; - protected Util $util; + protected Util&MockObject $util; protected IconBuilder $iconBuilder; protected function setUp(): void { @@ -36,123 +34,228 @@ class IconBuilderTest extends TestCase { $this->themingDefaults = $this->createMock(ThemingDefaults::class); $this->appManager = $this->createMock(IAppManager::class); $this->imageManager = $this->createMock(ImageManager::class); - $this->util = new Util($this->createMock(ServerVersion::class), $this->config, $this->appManager, $this->appData, $this->imageManager); + $this->util = $this->createMock(Util::class); $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util, $this->imageManager); } - private function checkImagick() { + /** + * Checks if Imagick and the required format are available. + * If provider is null, only checks for Imagick extension. + */ + private function checkImagick(?string $provider = null) { if (!extension_loaded('imagick')) { $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); } - $checkImagick = new \Imagick(); - if (count($checkImagick->queryFormats('SVG')) < 1) { - $this->markTestSkipped('No SVG provider present.'); - } - if (count($checkImagick->queryFormats('PNG')) < 1) { - $this->markTestSkipped('No PNG provider present.'); + if ($provider !== null) { + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats($provider)) < 1) { + $this->markTestSkipped('Imagemagick ' . $provider . ' support is required for this icon generation test.'); + } } } - public static function dataRenderAppIcon(): array { + /** + * Data provider for app icon rendering tests (SVG only). + */ + public static function dataRenderAppIconSvg(): array { return [ - ['core', '#0082c9', 'touch-original.png'], - ['core', '#FF0000', 'touch-core-red.png'], - ['testing', '#FF0000', 'touch-testing-red.png'], - ['comments', '#0082c9', 'touch-comments.png'], - ['core', '#0082c9', 'touch-original-png.png'], + ['logo', '#0082c9', 'logo.svg'], + ['settings', '#FF0000', 'settings.svg'], ]; } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataRenderAppIcon')] - public function testRenderAppIcon(string $app, string $color, string $file): void { - $this->checkImagick(); - $this->themingDefaults->expects($this->once()) + /** + * Data provider for app icon rendering tests (PNG only). + */ + public static function dataRenderAppIconPng(): array { + return [ + ['logo', '#0082c9', 'logo.png'], + ['settings', '#FF0000', 'settings.png'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')] + public function testRenderAppIconSvg(string $app, string $color, string $file): void { + $this->checkImagick('SVG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['SVG', true], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, true) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) ->method('getColorPrimary') ->willReturn($color); - $this->appData->expects($this->once()) - ->method('getFolder') - ->with('global/images') - ->willThrowException(new NotFoundException()); - - $expectedIcon = new \Imagick(realpath(__DIR__) . '/data/' . $file); + // generate expected output from source file + $expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color); + // run test $icon = $this->iconBuilder->renderAppIcon($app, 512); - $this->assertEquals(true, $icon->valid()); $this->assertEquals(512, $icon->getImageWidth()); $this->assertEquals(512, $icon->getImageHeight()); - $this->assertEquals($icon, $expectedIcon); + $icon->setImageFormat('SVG'); + $expectedIcon->setImageFormat('SVG'); + $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected'); $icon->destroy(); $expectedIcon->destroy(); - // FIXME: We may need some comparison of the generated and the test images - // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataRenderAppIcon')] - public function testGetTouchIcon(string $app, string $color, string $file): void { - $this->checkImagick(); - $this->themingDefaults->expects($this->once()) + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')] + public function testRenderAppIconPng(string $app, string $color, string $file): void { + $this->checkImagick('PNG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['SVG', false], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, false) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) ->method('getColorPrimary') ->willReturn($color); - $this->appData->expects($this->once()) - ->method('getFolder') - ->with('global/images') - ->willThrowException(new NotFoundException()); - - $expectedIcon = new \Imagick(realpath(__DIR__) . '/data/' . $file); - $icon = new \Imagick(); - $icon->readImageBlob($this->iconBuilder->getTouchIcon($app)); - + // generate expected output from source file + $expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color); + // run test + $icon = $this->iconBuilder->renderAppIcon($app, 512); $this->assertEquals(true, $icon->valid()); $this->assertEquals(512, $icon->getImageWidth()); $this->assertEquals(512, $icon->getImageHeight()); - $this->assertEquals($icon, $expectedIcon); + $icon->setImageFormat('PNG'); + $expectedIcon->setImageFormat('PNG'); + $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected'); $icon->destroy(); $expectedIcon->destroy(); - // FIXME: We may need some comparison of the generated and the test images - // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataRenderAppIcon')] - public function testGetFavicon(string $app, string $color, string $file): void { - $this->checkImagick(); - $this->imageManager->expects($this->once()) - ->method('shouldReplaceIcons') - ->willReturn(true); - $this->themingDefaults->expects($this->once()) + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')] + public function testGetTouchIconSvg(string $app, string $color, string $file): void { + $this->checkImagick('SVG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['SVG', true], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, true) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) ->method('getColorPrimary') ->willReturn($color); - $this->appData->expects($this->once()) - ->method('getFolder') - ->with('global/images') - ->willThrowException(new NotFoundException()); - - $expectedIcon = new \Imagick(realpath(__DIR__) . '/data/' . $file); - $actualIcon = $this->iconBuilder->getFavicon($app); - - $icon = new \Imagick(); - $icon->setFormat('ico'); - $icon->readImageBlob($actualIcon); - - $this->assertEquals(true, $icon->valid()); - $this->assertEquals(128, $icon->getImageWidth()); - $this->assertEquals(128, $icon->getImageHeight()); - $icon->destroy(); + // generate expected output from source file + $expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color); + $expectedIcon->setImageFormat('PNG32'); + // run test + $result = $this->iconBuilder->getTouchIcon($app); + $this->assertIsString($result, 'Touch icon generation should return a PNG blob'); + $this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected'); + $expectedIcon->destroy(); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')] + public function testGetTouchIconPng(string $app, string $color, string $file): void { + $this->checkImagick('PNG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['SVG', false], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, false) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) + ->method('getColorPrimary') + ->willReturn($color); + // generate expected output from source file + $expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color); + $expectedIcon->setImageFormat('PNG32'); + // run test + $result = $this->iconBuilder->getTouchIcon($app); + $this->assertIsString($result, 'Touch icon generation should return a PNG blob'); + $this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected'); + $expectedIcon->destroy(); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')] + public function testGetFavIconSvg(string $app, string $color, string $file): void { + $this->checkImagick('SVG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['ICO', true], + ['SVG', true], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, true) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) + ->method('getColorPrimary') + ->willReturn($color); + // generate expected output from source file + $expectedIcon = $this->generateTestFavIcon($file, 'SVG', $color); + // run test + $result = $this->iconBuilder->getFavicon($app); + $this->assertIsString($result, 'Favicon generation should return a ICO blob'); + $this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected'); + $expectedIcon->destroy(); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')] + public function testGetFaviconPng(string $app, string $color, string $file): void { + $this->checkImagick('PNG'); + // mock required methods + $this->imageManager->expects($this->any()) + ->method('canConvert') + ->willReturnMap([ + ['ICO', true], + ['SVG', false], + ['PNG', true] + ]); + $this->util->expects($this->once()) + ->method('getAppIcon') + ->with($app, false) + ->willReturn(__DIR__ . '/data/' . $file); + $this->themingDefaults->expects($this->any()) + ->method('getColorPrimary') + ->willReturn($color); + // generate expected output from source file + $expectedIcon = $this->generateTestFavIcon($file, 'PNG', $color); + // run test + $result = $this->iconBuilder->getFavicon($app); + $this->assertIsString($result, 'Favicon generation should return a ICO blob'); + $this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected'); $expectedIcon->destroy(); - // FIXME: We may need some comparison of the generated and the test images - // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) } public function testGetFaviconNotFound(): void { - $this->checkImagick(); + $this->checkImagick('ICO'); $util = $this->createMock(Util::class); $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager); - $this->imageManager->expects($this->once()) - ->method('shouldReplaceIcons') + $this->imageManager->expects($this->any()) + ->method('canConvert') ->willReturn(true); $util->expects($this->once()) ->method('getAppIcon') ->willReturn('notexistingfile'); - $this->assertFalse($iconBuilder->getFavicon('noapp')); + $result = $iconBuilder->getFavicon('noapp'); + $this->assertFalse($result, 'Favicon generation should fail for missing file'); } public function testGetTouchIconNotFound(): void { @@ -174,4 +277,85 @@ class IconBuilderTest extends TestCase { ->willReturn('notexistingfile'); $this->assertFalse($iconBuilder->colorSvg('noapp', 'noimage')); } + + /** + * Helper to generate expected icon from source file for tests. + */ + private function generateTestIcon(string $file, string $format, int $size, string $color): \Imagick { + $filePath = realpath(__DIR__ . '/data/' . $file); + $appIconFile = new \Imagick(); + if ($format === 'SVG') { + $svgContent = file_get_contents($filePath); + if (substr($svgContent, 0, 5) !== '' . $svgContent; + } + // get dimensions for resolution calculation + $tmp = new \Imagick(); + $tmp->setBackgroundColor(new \ImagickPixel('transparent')); + $tmp->setResolution(72, 72); + $tmp->readImageBlob($svgContent); + $x = $tmp->getImageWidth(); + $y = $tmp->getImageHeight(); + $tmp->destroy(); + // set resolution for proper scaling + $resX = (int)(72 * $size / $x); + $resY = (int)(72 * $size / $y); + $appIconFile->setBackgroundColor(new \ImagickPixel('transparent')); + $appIconFile->setResolution($resX, $resY); + $appIconFile->readImageBlob($svgContent); + } else { + $appIconFile->readImage($filePath); + } + $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; + $appIconFile->resizeImage($new_w, $new_h, \Imagick::FILTER_LANCZOS, 1); + $finalIconFile = new \Imagick(); + $finalIconFile->setBackgroundColor(new \ImagickPixel('transparent')); + $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(); + $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); + $finalIconFile->setImageFormat('png'); + $appIconFile->destroy(); + return $finalIconFile; + } + + /** + * Helper to generate expected favicon from source file for tests. + */ + private function generateTestFavIcon(string $file, string $format, string $color): \Imagick { + $baseIcon = $this->generateTestIcon($file, $format, 128, $color); + $baseIcon->setImageFormat('PNG32'); + + $testIcon = new \Imagick(); + $testIcon->setFormat('ICO'); + foreach ([16, 32, 64, 128] as $size) { + $clone = clone $baseIcon; + $clone->scaleImage($size, 0); + $testIcon->addImage($clone); + $clone->destroy(); + } + $baseIcon->destroy(); + return $testIcon; + } } diff --git a/apps/theming/tests/data/favicon-original.ico b/apps/theming/tests/data/favicon-original.ico deleted file mode 100644 index fab2f7f0231e60b0db6981b999d62869179e070d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1618 zcmV-Y2CeytP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 diff --git a/apps/theming/tests/data/logo.svg.license b/apps/theming/tests/data/logo.svg.license new file mode 100644 index 00000000000..14e094ccb3f --- /dev/null +++ b/apps/theming/tests/data/logo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file diff --git a/apps/theming/tests/data/settings.png b/apps/theming/tests/data/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b9af15c7e98c0fa1ec874a86c43b719210d8fbaa GIT binary patch literal 3082 zcmd^>X;2eq7{}k;ESks_K{=F2A}B2i!BXT*hkon_XR7Uoo%jDf|J`|?+2{Y8 z-5>mYJ+*;w}y(FA>hl=`Z9^J$a*svBEb%G`nAO!c04m=pu$hZQ*UKTt7 z9h2Z5x#EusP;ZNZZe(~&gPe^YS=HENh{nw!Qgbj>1vrX(uJUgbuvaaBZs?0z4bdpv zBQnAne{ZT%2bR0Y6ANXCYSMD@NO@V%YZ}I_C{^dhC+~0T+M}Q#qE*2A=e*HG1^rQ^ z`T%F7wRhZ;W7DKe$c!+>#F}FE5@SBHtIg?k82t9xRk1CHjE?Yogytd|R?H2V7=8MtUrnY0vrL?a$H>%#WJUPmK%7c~ z-M^WQe;8Q*6Y9L}-MS-#(KoeTv^xr-gD0}t|jRoqVj{ImEbbj+w%^3t3B-S1jyf3$A@XXZ4zmqWV;#^z=4a&ZJ zpgWX8kZAIbC8k|VS61t$tsc|Bw`*^88EnAO%LYVAbGfx(JFgthv{x&kv7|*fpz#EwaJt zYtyVE{vqdLW}8N$`a&);3|b!Vi-D=w;7{3sMzcC*tWiBPYH zg4mWJ4vxTcNhJOXueR~igZw1z6J|1tLt2b-MS+jnAbwj&L?9If8eaX&5dW~7(L@N& zpX&kG+bmCzsaTx z48NtAZA`Gr65f%{N+Xb%X-1<01bUZScW$v>rT+JZs^dVYl>>w|?D5+~renR8jyYzv z`;}S`xx?My7vTl`@`L4zVFz0lel<#qmSR$`VuuXyFm;!DBe_y#>|8>F)?Pi-7D<%U zh)9x}po@Z&Aj_=f4GO{u3|`5#$D;saM09+)xqd2O$Lddw~p3SrE57=0O5^X?$@3w>2{CHg4Rs%;@|~M z67Q5{$V}qoK0Gr5oZ8PGhbiN=`+Nc7f8}M2CP~t~eP^rFWM-Mq5pGo&k>8|Yno1LIyl+afMr$X0F@sVn#c4aLE3>?G7v zmGV%K^$w>nbYT{WrF6?MWwflj5Xp-|AlPVrgFPb?R2aX`Sx3dE_<<++(M4;jAxdhc z35?iStg!cBIzl)_FO!Hh_?IWa DDg)NG literal 0 HcmV?d00001 diff --git a/apps/theming/tests/data/settings.png.license b/apps/theming/tests/data/settings.png.license new file mode 100644 index 00000000000..14e094ccb3f --- /dev/null +++ b/apps/theming/tests/data/settings.png.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file diff --git a/apps/theming/tests/data/settings.svg b/apps/theming/tests/data/settings.svg new file mode 100644 index 00000000000..61f78599121 --- /dev/null +++ b/apps/theming/tests/data/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/theming/tests/data/settings.svg.license b/apps/theming/tests/data/settings.svg.license new file mode 100644 index 00000000000..14e094ccb3f --- /dev/null +++ b/apps/theming/tests/data/settings.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file diff --git a/apps/theming/tests/data/touch-comments.png b/apps/theming/tests/data/touch-comments.png deleted file mode 100644 index c4dc68d8238dbc0895c0cb8f0faad23eb0f923a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7145 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 ICC7;W1JmLApa1{> diff --git a/apps/theming/tests/data/touch-core-red.png b/apps/theming/tests/data/touch-core-red.png deleted file mode 100644 index 95e9a292e6e70c3a57edf8e9838a8c57d5329c7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8278 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%0Dt(ZtRbb9!_%2+r3OvHE5gL{{^gb4jyLrc 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 X^pnEXUfDYOI00Cg*cw+E5@P-j&!_;* diff --git a/openapi.json b/openapi.json index 6f30feb7404..93b4fbdf9be 100644 --- a/openapi.json +++ b/openapi.json @@ -34558,6 +34558,12 @@ "200": { "description": "Favicon returned", "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + }, "image/x-icon": { "schema": { "type": "string", @@ -34627,7 +34633,7 @@ "format": "binary" } }, - "image/x-icon": { + "*/*": { "schema": { "type": "string", "format": "binary"