diff options
author | Julius Haertl <jus@bitgrid.net> | 2016-08-30 09:03:06 +0200 |
---|---|---|
committer | Julius Haertl <jus@bitgrid.net> | 2016-11-18 10:23:24 +0100 |
commit | af8976ab03d976bba04a2442957a650728de7ecb (patch) | |
tree | 4d2d10e66a62f0999e2ec0d792823fdc8b53d18a /apps/theming | |
parent | b466628bfddee71b3c4d9f8d903e327269f57b4a (diff) | |
download | nextcloud-server-af8976ab03d976bba04a2442957a650728de7ecb.tar.gz nextcloud-server-af8976ab03d976bba04a2442957a650728de7ecb.zip |
Add IconBuilder class to encapsulate icon generation
Signed-off-by: Julius Haertl <jus@bitgrid.net>
Diffstat (limited to 'apps/theming')
-rw-r--r-- | apps/theming/lib/Controller/IconController.php | 117 | ||||
-rw-r--r-- | apps/theming/lib/IconBuilder.php | 140 | ||||
-rw-r--r-- | apps/theming/lib/ThemingDefaults.php | 17 | ||||
-rw-r--r-- | apps/theming/tests/Controller/IconControllerTest.php | 44 | ||||
-rw-r--r-- | apps/theming/tests/IconBuilderTest.php | 150 |
5 files changed, 341 insertions, 127 deletions
diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php index 5770bd20742..78d41d621a0 100644 --- a/apps/theming/lib/Controller/IconController.php +++ b/apps/theming/lib/Controller/IconController.php @@ -22,6 +22,7 @@ */ namespace OCA\Theming\Controller; +use OCA\Theming\IconBuilder; use OCA\Theming\Template; use OCA\Theming\ThemingDefaults; use OCP\AppFramework\Controller; @@ -35,9 +36,6 @@ use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; use OCA\Theming\Util; -use OCP\IURLGenerator; -use Imagick; -use ImagickPixel; class IconController extends Controller { /** @var ThemingDefaults */ @@ -52,7 +50,8 @@ class IconController extends Controller { private $config; /** @var IRootFolder */ private $rootFolder; - + /** @var IconBuilder */ + private $iconBuilder; /** * IconController constructor. @@ -84,6 +83,9 @@ class IconController extends Controller { $this->l = $l; $this->config = $config; $this->rootFolder = $rootFolder; + if(extension_loaded('imagick')) { + $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util); + } } /** @@ -91,7 +93,7 @@ class IconController extends Controller { * @NoCSRFRequired * * @param $app app name - * @param $image image file name + * @param $image image file name (svg required) * @return StreamResponse|DataResponse */ public function getThemedIcon($app, $image) { @@ -99,10 +101,10 @@ class IconController extends Controller { $svg = file_get_contents($image); $color = $this->util->elementColor($this->themingDefaults->getMailHeaderColor()); $svg = $this->util->colorizeSvg($svg, $color); - $response = new DataDisplayResponse($svg, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); $response->cacheFor(86400); $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + $response->addHeader('Pragma', 'cache'); return $response; } @@ -116,14 +118,21 @@ class IconController extends Controller { * @return StreamResponse|DataResponse */ public function getFavicon($app="core") { - $icon = $this->renderAppIcon($app); - $icon->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); - $icon->setImageFormat("png24"); + if($this->themingDefaults->shouldReplaceIcons()) { + $icon = $this->iconBuilder->getFavicon($app); + $response = new DataDisplayResponse($icon, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $response->cacheFor(86400); + $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + $response->addHeader('Pragma', 'cache'); + return $response; + } else { + $response = new DataDisplayResponse(null, Http::STATUS_NOT_FOUND); + $response->cacheFor(86400); + $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + return $response; + } + - $response = new DataDisplayResponse($icon, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - $response->cacheFor(86400); - $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); - return $response; } /** @@ -136,80 +145,20 @@ class IconController extends Controller { * @return StreamResponse|DataResponse */ public function getTouchIcon($app="core") { - $icon = $this->renderAppIcon($app); - $icon->setImageFormat("png24"); - - $response = new DataDisplayResponse($icon, Http::STATUS_OK, ['Content-Type' => 'image/png']); - $response->cacheFor(86400); - $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); - return $response; - } - - /** - * Render app icon on themed background color - * fallback to logo - * - * @param $app app name - * @return Imagick - */ - private function renderAppIcon($app) { - $appIcon = $this->util->getAppIcon($app); - $color = $this->themingDefaults->getMailHeaderColor(); - $mime = mime_content_type($appIcon); - // generate background image with rounded corners - $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="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink">' . - '<rect x="0" y="0" rx="75" ry="75" width="512" height="512" style="fill:' . $color . ';" />' . - '</svg>'; - - // resize svg magic as this seems broken in Imagemagick - if($mime === "image/svg+xml") { - $svg = file_get_contents($appIcon); - - $tmp = new Imagick(); - $tmp->readImageBlob($svg); - $x = $tmp->getImageWidth(); - $y = $tmp->getImageHeight(); - $res = $tmp->getImageResolution(); - $tmp->destroy(); - - // convert svg to resized image - $appIconFile = new Imagick(); - $resX = (int)(512 * $res['x'] / $x * 2.53); - $resY = (int)(512 * $res['y'] / $y * 2.53); - $appIconFile->setResolution($resX, $resY); - $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); - $appIconFile->readImageBlob($svg); + if($this->themingDefaults->shouldReplaceIcons()) { + $icon = $this->iconBuilder->getTouchIcon($app); + $response = new DataDisplayResponse($icon, Http::STATUS_OK, ['Content-Type' => 'image/png']); + $response->cacheFor(86400); + $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + $response->addHeader('Pragma', 'cache'); + return $response; } else { - $appIconFile = new Imagick(); - $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); - $appIconFile->readImageBlob(file_get_contents($appIcon)); - $appIconFile->scaleImage(512, 512, true); + $response = new DataDisplayResponse(null, Http::STATUS_NOT_FOUND); + $response->cacheFor(86400); + $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + return $response; } - - // offset for icon positioning - $border_w = (int)($appIconFile->getImageWidth() * 0.05); - $border_h = (int)($appIconFile->getImageHeight() * 0.05); - $innerWidth = (int)($appIconFile->getImageWidth() - $border_w * 2); - $innerHeight = (int)($appIconFile->getImageHeight() - $border_h * 2); - $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight); - // center icon - $offset_w = 512 / 2 - $innerWidth / 2; - $offset_h = 512 / 2 - $innerHeight / 2; - - $appIconFile->setImageFormat("png24"); - - $finalIconFile = new Imagick(); - $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->resizeImage(512, 512, Imagick::FILTER_LANCZOS, 1); - - $appIconFile->destroy(); - return $finalIconFile; } - }
\ No newline at end of file diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php new file mode 100644 index 00000000000..b61e12d9236 --- /dev/null +++ b/apps/theming/lib/IconBuilder.php @@ -0,0 +1,140 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Theming; + +use Imagick; +use ImagickPixel; + +class IconBuilder { + /** @var ThemingDefaults */ + private $themingDefaults; + /** @var Util */ + private $util; + + /** + * IconBuilder constructor. + * + * @param ThemingDefaults $themingDefaults + * @param Util $util + */ + public function __construct( + ThemingDefaults $themingDefaults, + Util $util + ) { + $this->themingDefaults = $themingDefaults; + $this->util = $util; + } + + /** + * @param $app app name + * @return string image blob + */ + public function getFavicon($app) { + $icon = $this->renderAppIcon($app); + $icon->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $icon->setImageFormat("png24"); + $data = $icon->getImageBlob(); + $icon->destroy(); + return $data; + } + + /** + * @param $app app name + * @return string image blob + */ + public function getTouchIcon($app) { + $icon = $this->renderAppIcon($app); + $icon->setImageFormat("png24"); + $data = $icon->getImageBlob(); + $icon->destroy(); + return $data; + } + + /** + * Render app icon on themed background color + * fallback to logo + * + * @param $app app name + * @return Imagick + */ + public function renderAppIcon($app) { + $appIcon = $this->util->getAppIcon($app); + + $color = $this->themingDefaults->getMailHeaderColor(); + $mime = mime_content_type($appIcon); + // generate background image with rounded corners + $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="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink">' . + '<rect x="0" y="0" rx="75" ry="75" width="512" height="512" style="fill:' . $color . ';" />' . + '</svg>'; + + // resize svg magic as this seems broken in Imagemagick + if($mime === "image/svg+xml") { + $svg = file_get_contents($appIcon); + + $tmp = new Imagick(); + $tmp->readImageBlob($svg); + $x = $tmp->getImageWidth(); + $y = $tmp->getImageHeight(); + $res = $tmp->getImageResolution(); + $tmp->destroy(); + + // convert svg to resized image + $appIconFile = new Imagick(); + $resX = (int)(512 * $res['x'] / $x * 2.53); + $resY = (int)(512 * $res['y'] / $y * 2.53); + $appIconFile->setResolution($resX, $resY); + $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); + $appIconFile->readImageBlob($svg); + } else { + $appIconFile = new Imagick(); + $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); + $appIconFile->readImageBlob(file_get_contents($appIcon)); + $appIconFile->scaleImage(512, 512, true); + } + + // offset for icon positioning + $border_w = (int)($appIconFile->getImageWidth() * 0.05); + $border_h = (int)($appIconFile->getImageHeight() * 0.05); + $innerWidth = (int)($appIconFile->getImageWidth() - $border_w * 2); + $innerHeight = (int)($appIconFile->getImageHeight() - $border_h * 2); + $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight); + // center icon + $offset_w = 512 / 2 - $innerWidth / 2; + $offset_h = 512 / 2 - $innerHeight / 2; + + $appIconFile->setImageFormat("png24"); + + $finalIconFile = new Imagick(); + $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->resizeImage(512, 512, Imagick::FILTER_LANCZOS, 1); + + $appIconFile->destroy(); + return $finalIconFile; + } + +}
\ No newline at end of file diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index 9139dd56247..b7968d0073f 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -145,6 +145,23 @@ class ThemingDefaults extends \OC_Defaults { } /** + * Check if Imagemagick is enabled and if SVG is supported + * otherwise we can't render custom icons + * + * @return bool + */ + public function shouldReplaceIcons() { + if(extension_loaded('imagick')) { + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats('SVG')) >= 1) { + return true; + } + $checkImagick->clear(); + } + return false; + } + + /** * Increases the cache buster key */ private function increaseCacheBuster() { diff --git a/apps/theming/tests/Controller/IconControllerTest.php b/apps/theming/tests/Controller/IconControllerTest.php index 22d4ae343a1..3443be60712 100644 --- a/apps/theming/tests/Controller/IconControllerTest.php +++ b/apps/theming/tests/Controller/IconControllerTest.php @@ -55,13 +55,7 @@ class IconControllerTest extends TestCase { public function setUp() { - if(!extension_loaded('imagick')) { - $this->markTestSkipped('Tests skipped as Imagemagick is required for dynamic icon generation.'); - } - $checkImagick = new \Imagick(); - if (count($checkImagick->queryFormats('SVG')) < 1) { - $this->markTestSkipped('No SVG provider present'); - } + $this->request = $this->getMockBuilder('OCP\IRequest')->getMock(); $this->config = $this->getMockBuilder('OCP\IConfig')->getMock(); @@ -149,42 +143,6 @@ class IconControllerTest extends TestCase { $this->assertEquals($expected, $favicon); } - /** - * @dataProvider dataRenderAppIcon - * @param $appicon - * @param $color - * @param $file - */ - public function testRenderAppIcon($app, $appicon, $color, $file) { - - $this->util->expects($this->once()) - ->method('getAppIcon') - ->with($app) - ->willReturn(\OC::$SERVERROOT . "/" . $appicon); - $this->themingDefaults->expects($this->once()) - ->method('getMailHeaderColor') - ->willReturn($color); - - $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/../data/" . $file); - - $icon = $this->invokePrivate($this->iconController, 'renderAppIcon', [$app]); - $this->assertEquals(true, $icon->valid()); - $this->assertEquals(512, $icon->getImageWidth()); - $this->assertEquals(512, $icon->getImageHeight()); - $this->assertEquals($icon, $expectedIcon); - //$this->assertLessThan(0.0005, $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]); - - } - - public function dataRenderAppIcon() { - return [ - ['core','core/img/logo.svg', '#0082c9', 'touch-original.png'], - ['core','core/img/logo.svg', '#FF0000', 'touch-core-red.png'], - ['testing','apps/testing/img/app.svg', '#FF0000', 'touch-testing-red.png'], - ['comments','apps/comments/img/comments.svg', '#0082c9', 'touch-comments.png'], - ['core','core/img/logo.png', '#0082c9', 'touch-original-png.png'], - ]; - } } diff --git a/apps/theming/tests/IconBuilderTest.php b/apps/theming/tests/IconBuilderTest.php new file mode 100644 index 00000000000..ffabb31df79 --- /dev/null +++ b/apps/theming/tests/IconBuilderTest.php @@ -0,0 +1,150 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\Theming\Tests; + +use OCA\Theming\IconBuilder; +use OCA\Theming\ThemingDefaults; +use OCA\Theming\Util; +use OCP\Files\IRootFolder; +use OCP\IConfig; +use Test\TestCase; + +class IconBuilderTest extends TestCase { + + /** @var IConfig */ + protected $config; + /** @var IRootFolder */ + protected $rootFolder; + /** @var ThemingDefaults */ + protected $themingDefaults; + /** @var Util */ + protected $util; + /** @var IconBuilder */ + protected $iconBuilder; + + protected function setUp() { + parent::setUp(); + + 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.'); + } + + $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock(); + $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); + $this->themingDefaults = $this->getMockBuilder('OCA\Theming\ThemingDefaults') + ->disableOriginalConstructor()->getMock(); + $this->util = new Util($this->config, $this->rootFolder); + $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util); + } + + public function dataRenderAppIcon() { + 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'], + ]; + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testRenderAppIcon($app, $color, $file) { + + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = $this->iconBuilder->renderAppIcon($app); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(512, $icon->getImageWidth()); + $this->assertEquals(512, $icon->getImageHeight()); + $this->assertEquals($icon, $expectedIcon); + $icon->destroy(); + $expectedIcon->destroy(); + //$this->assertLessThan(0.0005, $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]); + + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testGetTouchIcon($app, $color, $file) { + + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = new \Imagick(); + $icon->readImageBlob($this->iconBuilder->getTouchIcon($app)); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(512, $icon->getImageWidth()); + $this->assertEquals(512, $icon->getImageHeight()); + $this->assertEquals($icon, $expectedIcon); + $icon->destroy(); + $expectedIcon->destroy(); + //$this->assertLessThan(0.0005, $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]); + + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testGetFavicon($app, $color, $file) { + + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = new \Imagick(); + $icon->readImageBlob($this->iconBuilder->getFavicon($app)); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(32, $icon->getImageWidth()); + $this->assertEquals(32, $icon->getImageHeight()); + $icon->destroy(); + $expectedIcon->destroy(); + //$this->assertLessThan(0.0005, $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]); + + } + +} |