IconBuilder.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OCA\Theming;
  7. use Imagick;
  8. use ImagickPixel;
  9. use OCP\Files\SimpleFS\ISimpleFile;
  10. class IconBuilder {
  11. /** @var ThemingDefaults */
  12. private $themingDefaults;
  13. /** @var Util */
  14. private $util;
  15. /** @var ImageManager */
  16. private $imageManager;
  17. /**
  18. * IconBuilder constructor.
  19. *
  20. * @param ThemingDefaults $themingDefaults
  21. * @param Util $util
  22. * @param ImageManager $imageManager
  23. */
  24. public function __construct(
  25. ThemingDefaults $themingDefaults,
  26. Util $util,
  27. ImageManager $imageManager
  28. ) {
  29. $this->themingDefaults = $themingDefaults;
  30. $this->util = $util;
  31. $this->imageManager = $imageManager;
  32. }
  33. /**
  34. * @param $app string app name
  35. * @return string|false image blob
  36. */
  37. public function getFavicon($app) {
  38. if (!$this->imageManager->shouldReplaceIcons()) {
  39. return false;
  40. }
  41. try {
  42. $favicon = new Imagick();
  43. $favicon->setFormat('ico');
  44. $icon = $this->renderAppIcon($app, 128);
  45. if ($icon === false) {
  46. return false;
  47. }
  48. $icon->setImageFormat('png32');
  49. $clone = clone $icon;
  50. $clone->scaleImage(16, 0);
  51. $favicon->addImage($clone);
  52. $clone = clone $icon;
  53. $clone->scaleImage(32, 0);
  54. $favicon->addImage($clone);
  55. $clone = clone $icon;
  56. $clone->scaleImage(64, 0);
  57. $favicon->addImage($clone);
  58. $clone = clone $icon;
  59. $clone->scaleImage(128, 0);
  60. $favicon->addImage($clone);
  61. $data = $favicon->getImagesBlob();
  62. $favicon->destroy();
  63. $icon->destroy();
  64. $clone->destroy();
  65. return $data;
  66. } catch (\ImagickException $e) {
  67. return false;
  68. }
  69. }
  70. /**
  71. * @param $app string app name
  72. * @return string|false image blob
  73. */
  74. public function getTouchIcon($app) {
  75. try {
  76. $icon = $this->renderAppIcon($app, 512);
  77. if ($icon === false) {
  78. return false;
  79. }
  80. $icon->setImageFormat('png32');
  81. $data = $icon->getImageBlob();
  82. $icon->destroy();
  83. return $data;
  84. } catch (\ImagickException $e) {
  85. return false;
  86. }
  87. }
  88. /**
  89. * Render app icon on themed background color
  90. * fallback to logo
  91. *
  92. * @param $app string app name
  93. * @param $size int size of the icon in px
  94. * @return Imagick|false
  95. */
  96. public function renderAppIcon($app, $size) {
  97. $appIcon = $this->util->getAppIcon($app);
  98. if ($appIcon === false) {
  99. return false;
  100. }
  101. if ($appIcon instanceof ISimpleFile) {
  102. $appIconContent = $appIcon->getContent();
  103. $mime = $appIcon->getMimeType();
  104. } else {
  105. $appIconContent = file_get_contents($appIcon);
  106. $mime = mime_content_type($appIcon);
  107. }
  108. if ($appIconContent === false || $appIconContent === '') {
  109. return false;
  110. }
  111. $color = $this->themingDefaults->getColorPrimary();
  112. // generate background image with rounded corners
  113. $cornerRadius = 0.2 * $size;
  114. $background = '<?xml version="1.0" encoding="UTF-8"?>' .
  115. '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width="' . $size . '" height="' . $size . '" xmlns:xlink="http://www.w3.org/1999/xlink">' .
  116. '<rect x="0" y="0" rx="' . $cornerRadius . '" ry="' . $cornerRadius . '" width="' . $size. '" height="' . $size . '" style="fill:' . $color . ';" />' .
  117. '</svg>';
  118. // resize svg magic as this seems broken in Imagemagick
  119. if ($mime === 'image/svg+xml' || substr($appIconContent, 0, 4) === '<svg') {
  120. if (substr($appIconContent, 0, 5) !== '<?xml') {
  121. $svg = '<?xml version="1.0"?>'.$appIconContent;
  122. } else {
  123. $svg = $appIconContent;
  124. }
  125. $tmp = new Imagick();
  126. $tmp->setBackgroundColor(new ImagickPixel('transparent'));
  127. $tmp->setResolution(72, 72);
  128. $tmp->readImageBlob($svg);
  129. $x = $tmp->getImageWidth();
  130. $y = $tmp->getImageHeight();
  131. $tmp->destroy();
  132. // convert svg to resized image
  133. $appIconFile = new Imagick();
  134. $resX = (int)(72 * $size / $x);
  135. $resY = (int)(72 * $size / $y);
  136. $appIconFile->setResolution($resX, $resY);
  137. $appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  138. $appIconFile->readImageBlob($svg);
  139. /**
  140. * invert app icons for bright primary colors
  141. * the default nextcloud logo will not be inverted to black
  142. */
  143. if ($this->util->isBrightColor($color)
  144. && !$appIcon instanceof ISimpleFile
  145. && $app !== 'core'
  146. ) {
  147. $appIconFile->negateImage(false);
  148. }
  149. } else {
  150. $appIconFile = new Imagick();
  151. $appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  152. $appIconFile->readImageBlob($appIconContent);
  153. }
  154. // offset for icon positioning
  155. $padding = 0.15;
  156. $border_w = (int)($appIconFile->getImageWidth() * $padding);
  157. $border_h = (int)($appIconFile->getImageHeight() * $padding);
  158. $innerWidth = ($appIconFile->getImageWidth() - $border_w * 2);
  159. $innerHeight = ($appIconFile->getImageHeight() - $border_h * 2);
  160. $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight);
  161. // center icon
  162. $offset_w = (int)($size / 2 - $innerWidth / 2);
  163. $offset_h = (int)($size / 2 - $innerHeight / 2);
  164. $finalIconFile = new Imagick();
  165. $finalIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  166. $finalIconFile->readImageBlob($background);
  167. $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
  168. $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
  169. $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
  170. $finalIconFile->setImageFormat('png24');
  171. if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
  172. $filter = Imagick::INTERPOLATE_BICUBIC;
  173. } else {
  174. $filter = Imagick::FILTER_LANCZOS;
  175. }
  176. $finalIconFile->resizeImage($size, $size, $filter, 1, false);
  177. $appIconFile->destroy();
  178. return $finalIconFile;
  179. }
  180. /**
  181. * @param $app string app name
  182. * @param $image string relative path to svg file in app directory
  183. * @return string|false content of a colorized svg file
  184. */
  185. public function colorSvg($app, $image) {
  186. $imageFile = $this->util->getAppImage($app, $image);
  187. if ($imageFile === false || $imageFile === '') {
  188. return false;
  189. }
  190. $svg = file_get_contents($imageFile);
  191. if ($svg !== false && $svg !== '') {
  192. $color = $this->util->elementColor($this->themingDefaults->getColorPrimary());
  193. $svg = $this->util->colorizeSvg($svg, $color);
  194. return $svg;
  195. } else {
  196. return false;
  197. }
  198. }
  199. }