Util.php 9.0 KB

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