From b3eab7db01f8e6dc4c98a1f89171052d8bf762b5 Mon Sep 17 00:00:00 2001 From: Julius Haertl Date: Thu, 11 Aug 2016 23:25:41 +0200 Subject: [PATCH] Theming: Add dynamic icon and favicon endpoints Signed-off-by: Julius Haertl --- apps/theming/appinfo/routes.php | 13 + .../theming/lib/Controller/IconController.php | 241 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 apps/theming/lib/Controller/IconController.php diff --git a/apps/theming/appinfo/routes.php b/apps/theming/appinfo/routes.php index 4a8d4bac5bc..1894d810287 100644 --- a/apps/theming/appinfo/routes.php +++ b/apps/theming/appinfo/routes.php @@ -60,5 +60,18 @@ return ['routes' => [ 'url' => '/js/theming', 'verb' => 'GET', ], + [ + 'name' => 'Icon#getFavicon', + 'url' => '/favicon/{app}', + 'verb' => 'GET', + 'defaults' => array("app" => "core"), + ], + [ + 'name' => 'Icon#getThemedIcon', + 'url' => '/image/{app}/{image}', + 'verb' => 'GET', + 'defaults' => array("app" => "core"), + 'requirements' => array('image' => '.+') + ], ]]; diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php new file mode 100644 index 00000000000..e1690c039d3 --- /dev/null +++ b/apps/theming/lib/Controller/IconController.php @@ -0,0 +1,241 @@ + + * + * @author Julius Haertl + * + * @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 . + * + */ +namespace OCA\Theming\Controller; + +use OCA\Theming\Template; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\StreamResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IRootFolder; +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 Template */ + private $template; + /** @var Util */ + private $util; + /** @var ITimeFactory */ + private $timeFactory; + /** @var IL10N */ + private $l; + /** @var IConfig */ + private $config; + /** @var IRootFolder */ + private $rootFolder; + + /** + * IconController constructor. + * + * @param string $appName + * @param IRequest $request + * @param IConfig $config + * @param Template $template + * @param Util $util + * @param ITimeFactory $timeFactory + * @param IL10N $l + * @param IRootFolder $rootFolder + */ + public function __construct( + $appName, + IRequest $request, + IConfig $config, + Template $template, + Util $util, + ITimeFactory $timeFactory, + IL10N $l, + IRootFolder $rootFolder + ) { + parent::__construct($appName, $request); + + $this->template = $template; + $this->util = $util; + $this->timeFactory = $timeFactory; + $this->l = $l; + $this->config = $config; + $this->rootFolder = $rootFolder; + } + + /** + * @PublicPage + * @NoCSRFRequired + * + * @param $app app name + * @param $image image file name + * @return StreamResponse|DataResponse + */ + public function getThemedIcon($app, $image) { + $image = $this->getAppImage($app, $image); + $svg = file_get_contents($image); + $color = $this->template->getMailHeaderColor(); + $svg = $this->colorizeSvg($svg, $color); + return new DataDisplayResponse($svg, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); + } + + /** + * Return a 32x32 favicon as png + * + * @PublicPage + * @NoCSRFRequired + * + * @param $app app name + * @return StreamResponse|DataResponse + */ + public function getFavicon($app) { + // TODO: we need caching here + $icon = $this->renderAppIcon($app); + $icon->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $icon->setImageFormat("png24"); + + $response = new DataDisplayResponse($icon, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $response->cacheFor(3600); + $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); + return $response; + } + + private function renderAppIcon($app) { + $appIcon = $this->getAppIcon($app); + $color = $this->config->getAppValue($this->appName, 'color'); + if ($color === "") { + $color = '#0082c9'; + } + $svg = file_get_contents($appIcon); + if ($this->util->invertTextColor($color)) { + $svg = $this->svgInvert($svg); + } + + // generate background image with rounded corners + $background = '' . + '' . + '' . + ''; + + $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); + $appIconFile->setImageFormat("png24"); + + // offset for icon positioning + $offset_w = (int)($appIconFile->getImageWidth() * 0.05); + $offset_h = (int)($appIconFile->getImageHeight() * 0.05); + // center icon if it is not square + if ($x > $y) { + $offset_h += 512 / 2 - $appIconFile->getImageHeight() / 2; + } + if ($y > $x) { + $offset_h += 512 / 2 - $appIconFile->getImageHeight() / 2; + } + + $innerWidth = (int)($appIconFile->getImageWidth() - $offset_w * 2); + $innerHeight = (int)($appIconFile->getImageHeight() - $offset_h * 2); + $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight); + + $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); + $appIconFile->destroy(); + return $finalIconFile; + } + + private function getAppIcon($app) { + $appPath = \OC_App::getAppPath($app); + + $icon = $appPath . '/img/' . $app . '.svg'; + if(file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/app.svg'; + if(file_exists($icon)) { + return $icon; + } + + return \OC::$SERVERROOT . '/core/img/logo.svg'; + } + + private function getAppImage($app, $image) { + // TODO: add support for images in core/img/ + $appPath = \OC_App::getAppPath($app); + + if($app==="core") { + $icon = \OC::$SERVERROOT . '/core/img/' . $image; + if(file_exists($icon)) { + return $icon; + } + } + + $icon = $appPath . '/img/' . $image; + if(file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.svg'; + if(file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.png'; + if(file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.gif'; + if(file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.jpg'; + if(file_exists($icon)) { + return $icon; + } + } + + private function svgInvert($svg) { + $svg = preg_replace('/#(f{3,6})/i', '#REPLACECOLOR', $svg); + $svg = preg_replace('/#(0{3,6})/i', '#ffffff', $svg); + $svg = preg_replace('/#(REPLACECOLOR)/i', '#000000', $svg); + return $svg; + } + + private function colorizeSvg($svg, $color) { + $svg = preg_replace('/#0082c9/i', $color, $svg); + return $svg; + } + +} \ No newline at end of file -- 2.39.5