IconBuilder.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. $background = '<?xml version="1.0" encoding="UTF-8"?>' .
  114. '<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">' .
  115. '<rect x="0" y="0" rx="100" ry="100" width="512" height="512" style="fill:' . $color . ';" />' .
  116. '</svg>';
  117. // resize svg magic as this seems broken in Imagemagick
  118. if ($mime === "image/svg+xml" || substr($appIconContent, 0, 4) === "<svg") {
  119. if (substr($appIconContent, 0, 5) !== "<?xml") {
  120. $svg = "<?xml version=\"1.0\"?>".$appIconContent;
  121. } else {
  122. $svg = $appIconContent;
  123. }
  124. $tmp = new Imagick();
  125. $tmp->readImageBlob($svg);
  126. $x = $tmp->getImageWidth();
  127. $y = $tmp->getImageHeight();
  128. $res = $tmp->getImageResolution();
  129. $tmp->destroy();
  130. if ($x > $y) {
  131. $max = $x;
  132. } else {
  133. $max = $y;
  134. }
  135. // convert svg to resized image
  136. $appIconFile = new Imagick();
  137. $resX = (int)(512 * $res['x'] / $max * 2.53);
  138. $resY = (int)(512 * $res['y'] / $max * 2.53);
  139. $appIconFile->setResolution($resX, $resY);
  140. $appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  141. $appIconFile->readImageBlob($svg);
  142. /**
  143. * invert app icons for bright primary colors
  144. * the default nextcloud logo will not be inverted to black
  145. */
  146. if ($this->util->isBrightColor($color)
  147. && !$appIcon instanceof ISimpleFile
  148. && $app !== "core"
  149. ) {
  150. $appIconFile->negateImage(false);
  151. }
  152. $appIconFile->scaleImage(512, 512, true);
  153. } else {
  154. $appIconFile = new Imagick();
  155. $appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  156. $appIconFile->readImageBlob($appIconContent);
  157. $appIconFile->scaleImage(512, 512, true);
  158. }
  159. // offset for icon positioning
  160. $border_w = (int)($appIconFile->getImageWidth() * 0.05);
  161. $border_h = (int)($appIconFile->getImageHeight() * 0.05);
  162. $innerWidth = ($appIconFile->getImageWidth() - $border_w * 2);
  163. $innerHeight = ($appIconFile->getImageHeight() - $border_h * 2);
  164. $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight);
  165. // center icon
  166. $offset_w = (int)(512 / 2 - $innerWidth / 2);
  167. $offset_h = (int)(512 / 2 - $innerHeight / 2);
  168. $finalIconFile = new Imagick();
  169. $finalIconFile->setBackgroundColor(new ImagickPixel('transparent'));
  170. $finalIconFile->readImageBlob($background);
  171. $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
  172. $finalIconFile->setImageArtifact('compose:args', "1,0,-0.5,0.5");
  173. $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
  174. $finalIconFile->setImageFormat('png24');
  175. if (defined("Imagick::INTERPOLATE_BICUBIC") === true) {
  176. $filter = Imagick::INTERPOLATE_BICUBIC;
  177. } else {
  178. $filter = Imagick::FILTER_LANCZOS;
  179. }
  180. $finalIconFile->resizeImage($size, $size, $filter, 1, false);
  181. $appIconFile->destroy();
  182. return $finalIconFile;
  183. }
  184. /**
  185. * @param $app string app name
  186. * @param $image string relative path to svg file in app directory
  187. * @return string|false content of a colorized svg file
  188. */
  189. public function colorSvg($app, $image) {
  190. $imageFile = $this->util->getAppImage($app, $image);
  191. if ($imageFile === false || $imageFile === "") {
  192. return false;
  193. }
  194. $svg = file_get_contents($imageFile);
  195. if ($svg !== false && $svg !== "") {
  196. $color = $this->util->elementColor($this->themingDefaults->getColorPrimary());
  197. $svg = $this->util->colorizeSvg($svg, $color);
  198. return $svg;
  199. } else {
  200. return false;
  201. }
  202. }
  203. }