config = $this->createMock(IConfig::class); $this->appData = $this->createMock(AppData::class); $this->themingDefaults = $this->createMock(ThemingDefaults::class); $this->appManager = $this->createMock(IAppManager::class); $this->imageManager = $this->createMock(ImageManager::class); $this->util = $this->createMock(Util::class); $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util, $this->imageManager); } /** * 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.'); } if ($provider !== null) { $checkImagick = new \Imagick(); if (count($checkImagick->queryFormats($provider)) < 1) { $this->markTestSkipped('Imagemagick ' . $provider . ' support is required for this icon generation test.'); } } } /** * Data provider for app icon rendering tests (SVG only). */ public static function dataRenderAppIconSvg(): array { return [ ['logo', '#0082c9', 'logo.svg'], ['settings', '#FF0000', 'settings.svg'], ]; } /** * 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); // 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()); $icon->setImageFormat('SVG'); $expectedIcon->setImageFormat('SVG'); $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected'); $icon->destroy(); $expectedIcon->destroy(); } #[\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); // 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()); $icon->setImageFormat('PNG'); $expectedIcon->setImageFormat('PNG'); $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected'); $icon->destroy(); $expectedIcon->destroy(); } #[\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); // 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(); } public function testGetFaviconNotFound(): void { $this->checkImagick('ICO'); $util = $this->createMock(Util::class); $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager); $this->imageManager->expects($this->any()) ->method('canConvert') ->willReturn(true); $util->expects($this->once()) ->method('getAppIcon') ->willReturn('notexistingfile'); $result = $iconBuilder->getFavicon('noapp'); $this->assertFalse($result, 'Favicon generation should fail for missing file'); } public function testGetTouchIconNotFound(): void { $this->checkImagick(); $util = $this->createMock(Util::class); $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager); $util->expects($this->once()) ->method('getAppIcon') ->willReturn('notexistingfile'); $this->assertFalse($iconBuilder->getTouchIcon('noapp')); } public function testColorSvgNotFound(): void { $this->checkImagick(); $util = $this->createMock(Util::class); $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager); $util->expects($this->once()) ->method('getAppImage') ->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; } }