Util.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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 Mexitek\PHPColors\Color;
  8. use OCP\App\AppPathNotFoundException;
  9. use OCP\App\IAppManager;
  10. use OCP\Files\IAppData;
  11. use OCP\Files\NotFoundException;
  12. use OCP\Files\SimpleFS\ISimpleFile;
  13. use OCP\IConfig;
  14. use OCP\IUserSession;
  15. class Util {
  16. private IConfig $config;
  17. private IAppManager $appManager;
  18. private IAppData $appData;
  19. private ImageManager $imageManager;
  20. public function __construct(IConfig $config, IAppManager $appManager, IAppData $appData, ImageManager $imageManager) {
  21. $this->config = $config;
  22. $this->appManager = $appManager;
  23. $this->appData = $appData;
  24. $this->imageManager = $imageManager;
  25. }
  26. /**
  27. * Should we invert the text on this background color?
  28. * @param string $color rgb color value
  29. * @return bool
  30. */
  31. public function invertTextColor(string $color): bool {
  32. return $this->colorContrast($color, '#ffffff') < 4.5;
  33. }
  34. /**
  35. * Is this color too bright ?
  36. * @param string $color rgb color value
  37. * @return bool
  38. */
  39. public function isBrightColor(string $color): bool {
  40. $l = $this->calculateLuma($color);
  41. if ($l > 0.6) {
  42. return true;
  43. } else {
  44. return false;
  45. }
  46. }
  47. /**
  48. * get color for on-page elements:
  49. * theme color by default, grey if theme color is to bright
  50. * @param string $color
  51. * @param ?bool $brightBackground
  52. * @return string
  53. */
  54. public function elementColor($color, ?bool $brightBackground = null, ?string $backgroundColor = null, bool $highContrast = false) {
  55. if ($backgroundColor !== null) {
  56. $brightBackground = $brightBackground ?? $this->isBrightColor($backgroundColor);
  57. // Minimal amount that is possible to change the luminance
  58. $epsilon = 1.0 / 255.0;
  59. // Current iteration to prevent infinite loops
  60. $iteration = 0;
  61. // We need to keep blurred backgrounds in mind which might be mixed with the background
  62. $blurredBackground = $this->mix($backgroundColor, $brightBackground ? $color : '#ffffff', 66);
  63. $contrast = $this->colorContrast($color, $blurredBackground);
  64. // Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
  65. $minContrast = $highContrast ? 5.6 : 3.2;
  66. while ($contrast < $minContrast && $iteration++ < 100) {
  67. $hsl = Color::hexToHsl($color);
  68. $hsl['L'] = max(0, min(1, $hsl['L'] + ($brightBackground ? -$epsilon : $epsilon)));
  69. $color = '#' . Color::hslToHex($hsl);
  70. $contrast = $this->colorContrast($color, $blurredBackground);
  71. }
  72. return $color;
  73. }
  74. // Fallback for legacy calling
  75. $luminance = $this->calculateLuminance($color);
  76. if ($brightBackground !== false && $luminance > 0.8) {
  77. // If the color is too bright in bright mode, we fall back to a darkened color
  78. return $this->darken($color, 30);
  79. }
  80. if ($brightBackground !== true && $luminance < 0.2) {
  81. // If the color is too dark in dark mode, we fall back to a brightened color
  82. return $this->lighten($color, 30);
  83. }
  84. return $color;
  85. }
  86. public function mix(string $color1, string $color2, int $factor): string {
  87. $color = new Color($color1);
  88. return '#' . $color->mix($color2, $factor);
  89. }
  90. public function lighten(string $color, int $factor): string {
  91. $color = new Color($color);
  92. return '#' . $color->lighten($factor);
  93. }
  94. public function darken(string $color, int $factor): string {
  95. $color = new Color($color);
  96. return '#' . $color->darken($factor);
  97. }
  98. /**
  99. * Convert RGB to HSL
  100. *
  101. * Copied from cssphp, copyright Leaf Corcoran, licensed under MIT
  102. *
  103. * @param int $red
  104. * @param int $green
  105. * @param int $blue
  106. *
  107. * @return float[]
  108. */
  109. public function toHSL(int $red, int $green, int $blue): array {
  110. $color = new Color(Color::rgbToHex(['R' => $red, 'G' => $green, 'B' => $blue]));
  111. return array_values($color->getHsl());
  112. }
  113. /**
  114. * @param string $color rgb color value
  115. * @return float
  116. */
  117. public function calculateLuminance(string $color): float {
  118. [$red, $green, $blue] = $this->hexToRGB($color);
  119. $hsl = $this->toHSL($red, $green, $blue);
  120. return $hsl[2];
  121. }
  122. /**
  123. * Calculate the Luma according to WCAG 2
  124. * http://www.w3.org/TR/WCAG20/#relativeluminancedef
  125. * @param string $color rgb color value
  126. * @return float
  127. */
  128. public function calculateLuma(string $color): float {
  129. $rgb = $this->hexToRGB($color);
  130. // Normalize the values by converting to float and applying the rules from WCAG2.0
  131. $rgb = array_map(function (int $color) {
  132. $color = $color / 255.0;
  133. if ($color <= 0.03928) {
  134. return $color / 12.92;
  135. } else {
  136. return pow((($color + 0.055) / 1.055), 2.4);
  137. }
  138. }, $rgb);
  139. [$red, $green, $blue] = $rgb;
  140. return (0.2126 * $red + 0.7152 * $green + 0.0722 * $blue);
  141. }
  142. /**
  143. * Calculat the color contrast according to WCAG 2
  144. * http://www.w3.org/TR/WCAG20/#contrast-ratiodef
  145. * @param string $color1 The first color
  146. * @param string $color2 The second color
  147. */
  148. public function colorContrast(string $color1, string $color2): float {
  149. $luminance1 = $this->calculateLuma($color1) + 0.05;
  150. $luminance2 = $this->calculateLuma($color2) + 0.05;
  151. return max($luminance1, $luminance2) / min($luminance1, $luminance2);
  152. }
  153. /**
  154. * @param string $color rgb color value
  155. * @return int[]
  156. * @psalm-return array{0: int, 1: int, 2: int}
  157. */
  158. public function hexToRGB(string $color): array {
  159. $color = new Color($color);
  160. return array_values($color->getRgb());
  161. }
  162. /**
  163. * @param $color
  164. * @return string base64 encoded radio button svg
  165. */
  166. public function generateRadioButton($color) {
  167. $radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">' .
  168. '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="'.$color.'"/></svg>';
  169. return base64_encode($radioButtonIcon);
  170. }
  171. /**
  172. * @param string $app app name
  173. * @return string|ISimpleFile path to app icon / file of logo
  174. */
  175. public function getAppIcon($app) {
  176. $app = str_replace(['\0', '/', '\\', '..'], '', $app);
  177. try {
  178. $appPath = $this->appManager->getAppPath($app);
  179. $icon = $appPath . '/img/' . $app . '.svg';
  180. if (file_exists($icon)) {
  181. return $icon;
  182. }
  183. $icon = $appPath . '/img/app.svg';
  184. if (file_exists($icon)) {
  185. return $icon;
  186. }
  187. } catch (AppPathNotFoundException $e) {
  188. }
  189. if ($this->config->getAppValue('theming', 'logoMime', '') !== '') {
  190. $logoFile = null;
  191. try {
  192. $folder = $this->appData->getFolder('global/images');
  193. return $folder->getFile('logo');
  194. } catch (NotFoundException $e) {
  195. }
  196. }
  197. return \OC::$SERVERROOT . '/core/img/logo/logo.svg';
  198. }
  199. /**
  200. * @param string $app app name
  201. * @param string $image relative path to image in app folder
  202. * @return string|false absolute path to image
  203. */
  204. public function getAppImage($app, $image) {
  205. $app = str_replace(['\0', '/', '\\', '..'], '', $app);
  206. $image = str_replace(['\0', '\\', '..'], '', $image);
  207. if ($app === 'core') {
  208. $icon = \OC::$SERVERROOT . '/core/img/' . $image;
  209. if (file_exists($icon)) {
  210. return $icon;
  211. }
  212. }
  213. try {
  214. $appPath = $this->appManager->getAppPath($app);
  215. } catch (AppPathNotFoundException $e) {
  216. return false;
  217. }
  218. $icon = $appPath . '/img/' . $image;
  219. if (file_exists($icon)) {
  220. return $icon;
  221. }
  222. $icon = $appPath . '/img/' . $image . '.svg';
  223. if (file_exists($icon)) {
  224. return $icon;
  225. }
  226. $icon = $appPath . '/img/' . $image . '.png';
  227. if (file_exists($icon)) {
  228. return $icon;
  229. }
  230. $icon = $appPath . '/img/' . $image . '.gif';
  231. if (file_exists($icon)) {
  232. return $icon;
  233. }
  234. $icon = $appPath . '/img/' . $image . '.jpg';
  235. if (file_exists($icon)) {
  236. return $icon;
  237. }
  238. return false;
  239. }
  240. /**
  241. * replace default color with a custom one
  242. *
  243. * @param string $svg content of a svg file
  244. * @param string $color color to match
  245. * @return string
  246. */
  247. public function colorizeSvg($svg, $color) {
  248. $svg = preg_replace('/#0082c9/i', $color, $svg);
  249. return $svg;
  250. }
  251. /**
  252. * Check if a custom theme is set in the server configuration
  253. *
  254. * @return bool
  255. */
  256. public function isAlreadyThemed() {
  257. $theme = $this->config->getSystemValue('theme', '');
  258. if ($theme !== '') {
  259. return true;
  260. }
  261. return false;
  262. }
  263. public function isBackgroundThemed() {
  264. $backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
  265. return $backgroundLogo !== '' && $backgroundLogo !== 'backgroundColor';
  266. }
  267. public function isLogoThemed() {
  268. return $this->imageManager->hasImage('logo')
  269. || $this->imageManager->hasImage('logoheader');
  270. }
  271. public function getCacheBuster(): string {
  272. $userSession = \OC::$server->get(IUserSession::class);
  273. $userId = '';
  274. $user = $userSession->getUser();
  275. if (!is_null($user)) {
  276. $userId = $user->getUID();
  277. }
  278. $serverVersion = \OC_Util::getVersionString();
  279. $themingAppVersion = $this->appManager->getAppVersion('theming');
  280. $userCacheBuster = '';
  281. if ($userId) {
  282. $userCacheBusterValue = (int)$this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
  283. $userCacheBuster = $userId . '_' . $userCacheBusterValue;
  284. }
  285. $systemCacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
  286. return substr(sha1($serverVersion . $themingAppVersion . $userCacheBuster . $systemCacheBuster), 0, 8);
  287. }
  288. }