IconBuilder.php 6.0 KB

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