aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming
diff options
context:
space:
mode:
authorJulius Haertl <jus@bitgrid.net>2016-08-30 09:03:06 +0200
committerJulius Haertl <jus@bitgrid.net>2016-11-18 10:23:24 +0100
commitaf8976ab03d976bba04a2442957a650728de7ecb (patch)
tree4d2d10e66a62f0999e2ec0d792823fdc8b53d18a /apps/theming
parentb466628bfddee71b3c4d9f8d903e327269f57b4a (diff)
downloadnextcloud-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.php117
-rw-r--r--apps/theming/lib/IconBuilder.php140
-rw-r--r--apps/theming/lib/ThemingDefaults.php17
-rw-r--r--apps/theming/tests/Controller/IconControllerTest.php44
-rw-r--r--apps/theming/tests/IconBuilderTest.php150
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]);
+
+ }
+
+}