color.module 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. <?php
  2. /**
  3. * @file
  4. * Allows users to change the color scheme of themes.
  5. */
  6. use Drupal\Core\Url;
  7. use Drupal\Component\Utility\Bytes;
  8. use Drupal\Component\Utility\Color;
  9. use Drupal\Component\Utility\Environment;
  10. use Drupal\Core\Asset\CssOptimizer;
  11. use Drupal\Core\Block\BlockPluginInterface;
  12. use Drupal\Core\File\Exception\FileException;
  13. use Drupal\Core\File\FileSystemInterface;
  14. use Drupal\Core\Form\FormStateInterface;
  15. use Drupal\Core\Language\LanguageInterface;
  16. use Drupal\Core\Render\Element\Textfield;
  17. use Drupal\Core\Routing\RouteMatchInterface;
  18. use Drupal\color\ColorSystemBrandingBlockAlter;
  19. /**
  20. * Implements hook_help().
  21. */
  22. function color_help($route_name, RouteMatchInterface $route_match) {
  23. switch ($route_name) {
  24. case 'help.page.color':
  25. $output = '<h3>' . t('About') . '</h3>';
  26. $output .= '<p>' . t('The Color module allows users with the <em>Administer site configuration</em> permission to change the color scheme (color of links, backgrounds, text, and other theme elements) of compatible themes. For more information, see the <a href=":color_do">online documentation for the Color module</a>.', [':color_do' => 'https://www.drupal.org/documentation/modules/color']) . '</p>';
  27. $output .= '<h3>' . t('Uses') . '</h3>';
  28. $output .= '<dl>';
  29. $output .= '<dt>' . t('Changing colors') . '</dt>';
  30. $output .= '<dd><p>' . t('To change the color settings, select the <em>Settings</em> link for your theme on the <a href=":appearance">Appearance</a> page. If the color picker does not appear then the theme is not compatible with the Color module.', [':appearance' => Url::fromRoute('system.themes_page')->toString()]) . '</p>';
  31. $output .= '<p>' . t("The Color module saves a modified copy of the theme's specified stylesheets in the files directory. If you make any manual changes to your theme's stylesheet, <em>you must save your color settings again, even if you haven't changed the colors</em>. This step is required because the module stylesheets in the files directory need to be recreated to reflect your changes.") . '</p></dd>';
  32. $output .= '</dl>';
  33. return $output;
  34. }
  35. }
  36. /**
  37. * Implements hook_theme().
  38. */
  39. function color_theme() {
  40. return [
  41. 'color_scheme_form' => [
  42. 'render element' => 'form',
  43. ],
  44. ];
  45. }
  46. /**
  47. * Implements hook_form_FORM_ID_alter().
  48. */
  49. function color_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) {
  50. $build_info = $form_state->getBuildInfo();
  51. if (isset($build_info['args'][0]) && ($theme = $build_info['args'][0]) && color_get_info($theme) && function_exists('gd_info')) {
  52. $form['color'] = [
  53. '#type' => 'details',
  54. '#title' => t('Color scheme'),
  55. '#open' => TRUE,
  56. '#weight' => -1,
  57. '#attributes' => ['id' => 'color_scheme_form'],
  58. '#theme' => 'color_scheme_form',
  59. ];
  60. $form['color'] += color_scheme_form($form, $form_state, $theme);
  61. $form['#validate'][] = 'color_scheme_form_validate';
  62. // Ensure color submission happens first so we can unset extra values.
  63. array_unshift($form['#submit'], 'color_scheme_form_submit');
  64. }
  65. }
  66. /**
  67. * Implements hook_library_info_alter().
  68. *
  69. * Replaces style sheets declared in libraries with color-altered style sheets.
  70. */
  71. function color_library_info_alter(&$libraries, $extension) {
  72. $themes = array_keys(\Drupal::service('theme_handler')->listInfo());
  73. if (in_array($extension, $themes)) {
  74. $color_paths = \Drupal::config('color.theme.' . $extension)->get('stylesheets');
  75. if (!empty($color_paths)) {
  76. foreach (array_keys($libraries) as $name) {
  77. if (isset($libraries[$name]['css'])) {
  78. // Override stylesheets.
  79. foreach ($libraries[$name]['css'] as $category => $css_assets) {
  80. foreach ($css_assets as $path => $metadata) {
  81. // Loop over the path array with recolored CSS files to find matching
  82. // paths which could replace the non-recolored paths.
  83. foreach ($color_paths as $color_path) {
  84. // Color module currently requires unique file names to be used,
  85. // which allows us to compare different file paths.
  86. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  87. $file_system = \Drupal::service('file_system');
  88. if ($file_system->basename($path) == $file_system->basename($color_path)) {
  89. // Replace the path to the new css file.
  90. // This keeps the order of the stylesheets intact.
  91. $index = array_search($path, array_keys($libraries[$name]['css'][$category]));
  92. $preceding_css_assets = array_slice($libraries[$name]['css'][$category], 0, $index);
  93. $succeeding_css_assets = array_slice($libraries[$name]['css'][$category], $index + 1);
  94. $libraries[$name]['css'][$category] = array_merge(
  95. $preceding_css_assets,
  96. [$color_path => $metadata],
  97. $succeeding_css_assets
  98. );
  99. }
  100. }
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }
  107. }
  108. /**
  109. * Implements hook_block_view_BASE_BLOCK_ID_alter().
  110. */
  111. function color_block_view_system_branding_block_alter(array &$build, BlockPluginInterface $block) {
  112. $build['#pre_render'][] = [ColorSystemBrandingBlockAlter::class, 'preRender'];
  113. }
  114. /**
  115. * #pre_render callback: Sets color preset logo.
  116. *
  117. * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
  118. * \Drupal\color\ColorSystemBrandingBlockAlter::preRender() instead.
  119. *
  120. * @see https://www.drupal.org/node/2966725
  121. */
  122. function color_block_view_pre_render(array $build) {
  123. @trigger_error('color_block_view_pre_render() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\color\ColorSystemBrandingBlockAlter::preRender() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED);
  124. return ColorSystemBrandingBlockAlter::preRender($build);
  125. }
  126. /**
  127. * Retrieves the Color module information for a particular theme.
  128. */
  129. function color_get_info($theme) {
  130. static $theme_info = [];
  131. if (isset($theme_info[$theme])) {
  132. return $theme_info[$theme];
  133. }
  134. $path = drupal_get_path('theme', $theme);
  135. $file = \Drupal::root() . '/' . $path . '/color/color.inc';
  136. if ($path && file_exists($file)) {
  137. include $file;
  138. // Add in default values.
  139. $info += [
  140. // CSS files (excluding @import) to rewrite with new color scheme.
  141. 'css' => [],
  142. // Files to copy.
  143. 'copy' => [],
  144. // Gradient definitions.
  145. 'gradients' => [],
  146. // Color areas to fill (x, y, width, height).
  147. 'fill' => [],
  148. // Coordinates of all the theme slices (x, y, width, height) with their
  149. // filename as used in the stylesheet.
  150. 'slices' => [],
  151. // Reference color used for blending.
  152. 'blend_target' => '#ffffff',
  153. ];
  154. $theme_info[$theme] = $info;
  155. return $info;
  156. }
  157. }
  158. /**
  159. * Retrieves the color palette for a particular theme.
  160. */
  161. function color_get_palette($theme, $default = FALSE) {
  162. // Fetch and expand default palette.
  163. $info = color_get_info($theme);
  164. $palette = $info['schemes']['default']['colors'];
  165. if ($default) {
  166. return $palette;
  167. }
  168. // Load variable.
  169. // @todo Default color config should be moved to yaml in the theme.
  170. // Getting a mutable override-free object because this function is only used
  171. // in forms. Color configuration is used to write CSS to the file system
  172. // making configuration overrides pointless.
  173. return \Drupal::configFactory()->getEditable('color.theme.' . $theme)->get('palette') ?: $palette;
  174. }
  175. /**
  176. * Form constructor for the color configuration form for a particular theme.
  177. *
  178. * @param $theme
  179. * The machine name of the theme whose color settings are being configured.
  180. *
  181. * @see color_scheme_form_validate()
  182. * @see color_scheme_form_submit()
  183. */
  184. function color_scheme_form($complete_form, FormStateInterface $form_state, $theme) {
  185. $info = color_get_info($theme);
  186. $info['schemes'][''] = ['title' => t('Custom'), 'colors' => []];
  187. $color_sets = [];
  188. $schemes = [];
  189. foreach ($info['schemes'] as $key => $scheme) {
  190. $color_sets[$key] = $scheme['title'];
  191. $schemes[$key] = $scheme['colors'];
  192. $schemes[$key] += $info['schemes']['default']['colors'];
  193. }
  194. // See if we're using a predefined scheme.
  195. // Note: we use the original theme when the default scheme is chosen.
  196. // Note: we use configuration without overrides since this information is used
  197. // in a form and therefore without doing this would bleed overrides into
  198. // active configuration. Furthermore, color configuration is used to write
  199. // CSS to the file system making configuration overrides pointless.
  200. $current_scheme = \Drupal::configFactory()->getEditable('color.theme.' . $theme)->get('palette');
  201. foreach ($schemes as $key => $scheme) {
  202. if ($current_scheme == $scheme) {
  203. $scheme_name = $key;
  204. break;
  205. }
  206. }
  207. if (empty($scheme_name)) {
  208. if (empty($current_scheme)) {
  209. $scheme_name = 'default';
  210. }
  211. else {
  212. $scheme_name = '';
  213. }
  214. }
  215. // Add scheme selector.
  216. $default_palette = color_get_palette($theme, TRUE);
  217. $form['scheme'] = [
  218. '#type' => 'select',
  219. '#title' => t('Color set'),
  220. '#options' => $color_sets,
  221. '#default_value' => $scheme_name,
  222. '#attached' => [
  223. 'library' => [
  224. 'color/drupal.color',
  225. 'color/admin',
  226. ],
  227. // Add custom JavaScript.
  228. 'drupalSettings' => [
  229. 'color' => [
  230. 'reference' => $default_palette,
  231. 'schemes' => $schemes,
  232. ],
  233. 'gradients' => $info['gradients'],
  234. ],
  235. ],
  236. ];
  237. // Add palette fields. Use the configuration if available.
  238. $palette = $current_scheme ?: $default_palette;
  239. $names = $info['fields'];
  240. $form['palette']['#tree'] = TRUE;
  241. foreach ($palette as $name => $value) {
  242. if (isset($names[$name])) {
  243. $form['palette'][$name] = [
  244. '#type' => 'textfield',
  245. '#title' => $names[$name],
  246. '#value_callback' => 'color_palette_color_value',
  247. '#default_value' => $value,
  248. '#size' => 8,
  249. '#attributes' => ['dir' => LanguageInterface::DIRECTION_LTR],
  250. ];
  251. }
  252. }
  253. $form['theme'] = ['#type' => 'value', '#value' => $theme];
  254. if (isset($info['#attached'])) {
  255. $form['#attached'] = $info['#attached'];
  256. unset($info['#attached']);
  257. }
  258. $form['info'] = ['#type' => 'value', '#value' => $info];
  259. return $form;
  260. }
  261. /**
  262. * Prepares variables for color scheme form templates.
  263. *
  264. * Default template: color-scheme-form.html.twig.
  265. *
  266. * @param array $variables
  267. * An associative array containing:
  268. * - form: A render element representing the form.
  269. */
  270. function template_preprocess_color_scheme_form(&$variables) {
  271. $form = &$variables['form'];
  272. $theme = $form['theme']['#value'];
  273. $info = $form['info']['#value'];
  274. if (isset($info['preview_library'])) {
  275. $form['scheme']['#attached']['library'][] = $info['preview_library'];
  276. }
  277. // Attempt to load preview HTML if the theme provides it.
  278. $preview_html_path = \Drupal::root() . '/' . (isset($info['preview_html']) ? drupal_get_path('theme', $theme) . '/' . $info['preview_html'] : drupal_get_path('module', 'color') . '/preview.html');
  279. $variables['html_preview']['#markup'] = file_get_contents($preview_html_path);
  280. }
  281. /**
  282. * Determines the value for a palette color field.
  283. *
  284. * @param array $element
  285. * The form element whose value is being populated.
  286. * @param string|bool $input
  287. * The incoming input to populate the form element. If this is FALSE,
  288. * the element's default value should be returned.
  289. * @param \Drupal\Core\Form\FormStateInterface $form_state
  290. * The current state of the form.
  291. *
  292. * @return string
  293. * The data that will appear in the $form_state->getValues() collection for this
  294. * element. Return nothing to use the default.
  295. */
  296. function color_palette_color_value($element, $input, FormStateInterface $form_state) {
  297. // If we suspect a possible cross-site request forgery attack, only accept
  298. // hexadecimal CSS color strings from user input, to avoid problems when this
  299. // value is used in the JavaScript preview.
  300. if ($input !== FALSE) {
  301. // Start with the provided value for this textfield, and validate that if
  302. // necessary, falling back on the default value.
  303. $value = Textfield::valueCallback($element, $input, $form_state);
  304. $complete_form = $form_state->getCompleteForm();
  305. if (!$value || !isset($complete_form['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state->getValue('form_token'), $complete_form['#token'])) {
  306. return $value;
  307. }
  308. else {
  309. return $element['#default_value'];
  310. }
  311. }
  312. }
  313. /**
  314. * Determines if a hexadecimal CSS color string is valid.
  315. *
  316. * @param string $color
  317. * The string to check.
  318. *
  319. * @return bool
  320. * TRUE if the string is a valid hexadecimal CSS color string, or FALSE if it
  321. * isn't.
  322. */
  323. function color_valid_hexadecimal_string($color) {
  324. return (bool) preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color);
  325. }
  326. /**
  327. * Form validation handler for color_scheme_form().
  328. *
  329. * @see color_scheme_form_submit()
  330. */
  331. function color_scheme_form_validate($form, FormStateInterface $form_state) {
  332. // Only accept hexadecimal CSS color strings to avoid XSS upon use.
  333. foreach ($form_state->getValue('palette') as $key => $color) {
  334. if (!color_valid_hexadecimal_string($color)) {
  335. $form_state->setErrorByName('palette][' . $key, t('You must enter a valid hexadecimal color value for %name.', ['%name' => $form['color']['palette'][$key]['#title']]));
  336. }
  337. }
  338. }
  339. /**
  340. * Form submission handler for color_scheme_form().
  341. *
  342. * @see color_scheme_form_validate()
  343. */
  344. function color_scheme_form_submit($form, FormStateInterface $form_state) {
  345. // Avoid color settings spilling over to theme settings.
  346. $color_settings = ['theme', 'palette', 'scheme'];
  347. if ($form_state->hasValue('info')) {
  348. $color_settings[] = 'info';
  349. }
  350. foreach ($color_settings as $setting_name) {
  351. ${$setting_name} = $form_state->getValue($setting_name);
  352. $form_state->unsetValue($setting_name);
  353. }
  354. if (!isset($info)) {
  355. return;
  356. }
  357. $config = \Drupal::configFactory()->getEditable('color.theme.' . $theme);
  358. // Resolve palette.
  359. if ($scheme != '') {
  360. foreach ($palette as $key => $color) {
  361. if (isset($info['schemes'][$scheme]['colors'][$key])) {
  362. $palette[$key] = $info['schemes'][$scheme]['colors'][$key];
  363. }
  364. }
  365. $palette += $info['schemes']['default']['colors'];
  366. }
  367. // Make sure enough memory is available.
  368. if (isset($info['base_image'])) {
  369. // Fetch source image dimensions.
  370. $source = drupal_get_path('theme', $theme) . '/' . $info['base_image'];
  371. list($width, $height) = getimagesize($source);
  372. // We need at least a copy of the source and a target buffer of the same
  373. // size (both at 32bpp).
  374. $required = $width * $height * 8;
  375. // We intend to prevent color scheme changes if there isn't enough memory
  376. // available. memory_get_usage(TRUE) returns a more accurate number than
  377. // memory_get_usage(), therefore we won't inadvertently reject a color
  378. // scheme change based on a faulty memory calculation.
  379. $usage = memory_get_usage(TRUE);
  380. $memory_limit = ini_get('memory_limit');
  381. $size = Bytes::toInt($memory_limit);
  382. if (!Environment::checkMemoryLimit($usage + $required, $memory_limit)) {
  383. \Drupal::messenger()->addError(t('There is not enough memory available to PHP to change this theme\'s color scheme. You need at least %size more. Check the <a href="http://php.net/manual/ini.core.php#ini.sect.resource-limits">PHP documentation</a> for more information.', ['%size' => format_size($usage + $required - $size)]));
  384. return;
  385. }
  386. }
  387. $file_system = \Drupal::service('file_system');
  388. // Delete old files.
  389. $files = $config->get('files');
  390. if (isset($files)) {
  391. foreach ($files as $file) {
  392. @$file_system->unlink($file);
  393. }
  394. }
  395. if (isset($file) && $file = dirname($file)) {
  396. @\Drupal::service('file_system')->rmdir($file);
  397. }
  398. // No change in color config, use the standard theme from color.inc.
  399. if (implode(',', color_get_palette($theme, TRUE)) == implode(',', $palette)) {
  400. $config->delete();
  401. return;
  402. }
  403. // Prepare target locations for generated files.
  404. $id = $theme . '-' . substr(hash('sha256', serialize($palette) . microtime()), 0, 8);
  405. $paths['color'] = 'public://color';
  406. $paths['target'] = $paths['color'] . '/' . $id;
  407. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  408. $file_system = \Drupal::service('file_system');
  409. foreach ($paths as $path) {
  410. $file_system->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);
  411. }
  412. $paths['target'] = $paths['target'] . '/';
  413. $paths['id'] = $id;
  414. $paths['source'] = drupal_get_path('theme', $theme) . '/';
  415. $paths['files'] = $paths['map'] = [];
  416. // Save palette and logo location.
  417. $config
  418. ->set('palette', $palette)
  419. ->set('logo', $paths['target'] . 'logo.svg')
  420. ->save();
  421. // Copy over neutral images.
  422. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  423. $file_system = \Drupal::service('file_system');
  424. foreach ($info['copy'] as $file) {
  425. $base = $file_system->basename($file);
  426. $source = $paths['source'] . $file;
  427. try {
  428. $filepath = $file_system->copy($source, $paths['target'] . $base);
  429. }
  430. catch (FileException $e) {
  431. $filepath = FALSE;
  432. }
  433. $paths['map'][$file] = $base;
  434. $paths['files'][] = $filepath;
  435. }
  436. // Render new images, if image has been provided.
  437. if (isset($info['base_image'])) {
  438. _color_render_images($theme, $info, $paths, $palette);
  439. }
  440. // Rewrite theme stylesheets.
  441. $css = [];
  442. foreach ($info['css'] as $stylesheet) {
  443. // Build a temporary array with CSS files.
  444. $files = [];
  445. if (file_exists($paths['source'] . $stylesheet)) {
  446. $files[] = $stylesheet;
  447. }
  448. foreach ($files as $file) {
  449. $css_optimizer = new CssOptimizer();
  450. // Aggregate @imports recursively for each configured top level CSS file
  451. // without optimization. Aggregation and optimization will be
  452. // handled by drupal_build_css_cache() only.
  453. $style = $css_optimizer->loadFile($paths['source'] . $file, FALSE);
  454. // Return the path to where this CSS file originated from, stripping
  455. // off the name of the file at the end of the path.
  456. $css_optimizer->rewriteFileURIBasePath = base_path() . dirname($paths['source'] . $file) . '/';
  457. // Prefix all paths within this CSS file, ignoring absolute paths.
  458. $style = preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', [$css_optimizer, 'rewriteFileURI'], $style);
  459. // Rewrite stylesheet with new colors.
  460. $style = _color_rewrite_stylesheet($theme, $info, $paths, $palette, $style);
  461. $base_file = $file_system->basename($file);
  462. $css[] = $paths['target'] . $base_file;
  463. _color_save_stylesheet($paths['target'] . $base_file, $style, $paths);
  464. }
  465. }
  466. // Maintain list of files.
  467. $config
  468. ->set('stylesheets', $css)
  469. ->set('files', $paths['files'])
  470. ->save();
  471. }
  472. /**
  473. * Rewrites the stylesheet to match the colors in the palette.
  474. */
  475. function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) {
  476. // Prepare color conversion table.
  477. $conversion = $palette;
  478. foreach ($conversion as $k => $v) {
  479. $v = mb_strtolower($v);
  480. $conversion[$k] = Color::normalizeHexLength($v);
  481. }
  482. $default = color_get_palette($theme, TRUE);
  483. // Split off the "Don't touch" section of the stylesheet.
  484. $split = "Color Module: Don't touch";
  485. if (strpos($style, $split) !== FALSE) {
  486. list($style, $fixed) = explode($split, $style);
  487. }
  488. // Find all colors in the stylesheet and the chunks in between.
  489. $style = preg_split('/(#[0-9a-f]{6}|#[0-9a-f]{3})/i', $style, -1, PREG_SPLIT_DELIM_CAPTURE);
  490. $is_color = FALSE;
  491. $output = '';
  492. $base = 'base';
  493. // Iterate over all the parts.
  494. foreach ($style as $chunk) {
  495. if ($is_color) {
  496. $chunk = mb_strtolower($chunk);
  497. $chunk = Color::normalizeHexLength($chunk);
  498. // Check if this is one of the colors in the default palette.
  499. if ($key = array_search($chunk, $default)) {
  500. $chunk = $conversion[$key];
  501. }
  502. // Not a pre-set color. Extrapolate from the base.
  503. else {
  504. $chunk = _color_shift($palette[$base], $default[$base], $chunk, $info['blend_target']);
  505. }
  506. }
  507. else {
  508. // Determine the most suitable base color for the next color.
  509. // 'a' declarations. Use link.
  510. if (preg_match('@[^a-z0-9_-](a)[^a-z0-9_-][^/{]*{[^{]+$@i', $chunk)) {
  511. $base = 'link';
  512. }
  513. // 'color:' styles. Use text.
  514. elseif (preg_match('/(?<!-)color[^{:]*:[^{#]*$/i', $chunk)) {
  515. $base = 'text';
  516. }
  517. // Reset back to base.
  518. else {
  519. $base = 'base';
  520. }
  521. }
  522. $output .= $chunk;
  523. $is_color = !$is_color;
  524. }
  525. // Append fixed colors segment.
  526. if (isset($fixed)) {
  527. $output .= $fixed;
  528. }
  529. // Replace paths to images.
  530. foreach ($paths['map'] as $before => $after) {
  531. $before = base_path() . $paths['source'] . $before;
  532. $before = preg_replace('`(^|/)(?!../)([^/]+)/../`', '$1', $before);
  533. $output = str_replace($before, $after, $output);
  534. }
  535. return $output;
  536. }
  537. /**
  538. * Saves the rewritten stylesheet to disk.
  539. */
  540. function _color_save_stylesheet($file, $style, &$paths) {
  541. $filepath = \Drupal::service('file_system')->saveData($style, $file, FileSystemInterface::EXISTS_REPLACE);
  542. $paths['files'][] = $filepath;
  543. // Set standard file permissions for webserver-generated files.
  544. \Drupal::service('file_system')->chmod($file);
  545. }
  546. /**
  547. * Renders images that match a given palette.
  548. */
  549. function _color_render_images($theme, &$info, &$paths, $palette) {
  550. // Prepare template image.
  551. $source = $paths['source'] . '/' . $info['base_image'];
  552. $source = imagecreatefrompng($source);
  553. $width = imagesx($source);
  554. $height = imagesy($source);
  555. // Prepare target buffer.
  556. $target = imagecreatetruecolor($width, $height);
  557. imagealphablending($target, TRUE);
  558. // Fill regions of solid color.
  559. foreach ($info['fill'] as $color => $fill) {
  560. imagefilledrectangle($target, $fill[0], $fill[1], $fill[0] + $fill[2], $fill[1] + $fill[3], _color_gd($target, $palette[$color]));
  561. }
  562. // Render gradients.
  563. foreach ($info['gradients'] as $gradient) {
  564. // Get direction of the gradient.
  565. if (isset($gradient['direction']) && $gradient['direction'] == 'horizontal') {
  566. // Horizontal gradient.
  567. for ($x = 0; $x < $gradient['dimension'][2]; $x++) {
  568. $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $x / ($gradient['dimension'][2] - 1));
  569. imagefilledrectangle($target, ($gradient['dimension'][0] + $x), $gradient['dimension'][1], ($gradient['dimension'][0] + $x + 1), ($gradient['dimension'][1] + $gradient['dimension'][3]), $color);
  570. }
  571. }
  572. else {
  573. // Vertical gradient.
  574. for ($y = 0; $y < $gradient['dimension'][3]; $y++) {
  575. $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $y / ($gradient['dimension'][3] - 1));
  576. imagefilledrectangle($target, $gradient['dimension'][0], $gradient['dimension'][1] + $y, $gradient['dimension'][0] + $gradient['dimension'][2], $gradient['dimension'][1] + $y + 1, $color);
  577. }
  578. }
  579. }
  580. // Blend over template.
  581. imagecopy($target, $source, 0, 0, 0, 0, $width, $height);
  582. // Clean up template image.
  583. imagedestroy($source);
  584. // Cut out slices.
  585. foreach ($info['slices'] as $file => $coord) {
  586. list($x, $y, $width, $height) = $coord;
  587. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  588. $file_system = \Drupal::service('file_system');
  589. $base = $file_system->basename($file);
  590. $image = $file_system->realpath($paths['target'] . $base);
  591. // Cut out slice.
  592. if ($file == 'screenshot.png') {
  593. $slice = imagecreatetruecolor(150, 90);
  594. imagecopyresampled($slice, $target, 0, 0, $x, $y, 150, 90, $width, $height);
  595. \Drupal::configFactory()->getEditable('color.theme.' . $theme)
  596. ->set('screenshot', $image)
  597. ->save();
  598. }
  599. else {
  600. $slice = imagecreatetruecolor($width, $height);
  601. imagecopy($slice, $target, 0, 0, $x, $y, $width, $height);
  602. }
  603. // Save image.
  604. imagepng($slice, $image);
  605. imagedestroy($slice);
  606. $paths['files'][] = $image;
  607. // Set standard file permissions for webserver-generated files.
  608. $file_system->chmod($image);
  609. // Build before/after map of image paths.
  610. $paths['map'][$file] = $base;
  611. }
  612. // Clean up target buffer.
  613. imagedestroy($target);
  614. }
  615. /**
  616. * Shifts a given color, using a reference pair and a target blend color.
  617. *
  618. * Note: this function is significantly different from the JS version, as it
  619. * is written to match the blended images perfectly.
  620. *
  621. * Constraint: if (ref2 == target + (ref1 - target) * delta) for some fraction
  622. * delta then (return == target + (given - target) * delta).
  623. *
  624. * Loose constraint: Preserve relative positions in saturation and luminance
  625. * space.
  626. */
  627. function _color_shift($given, $ref1, $ref2, $target) {
  628. // We assume that ref2 is a blend of ref1 and target and find
  629. // delta based on the length of the difference vectors.
  630. // delta = 1 - |ref2 - ref1| / |white - ref1|
  631. $target = _color_unpack($target, TRUE);
  632. $ref1 = _color_unpack($ref1, TRUE);
  633. $ref2 = _color_unpack($ref2, TRUE);
  634. $numerator = 0;
  635. $denominator = 0;
  636. for ($i = 0; $i < 3; ++$i) {
  637. $numerator += ($ref2[$i] - $ref1[$i]) * ($ref2[$i] - $ref1[$i]);
  638. $denominator += ($target[$i] - $ref1[$i]) * ($target[$i] - $ref1[$i]);
  639. }
  640. $delta = ($denominator > 0) ? (1 - sqrt($numerator / $denominator)) : 0;
  641. // Calculate the color that ref2 would be if the assumption was true.
  642. for ($i = 0; $i < 3; ++$i) {
  643. $ref3[$i] = $target[$i] + ($ref1[$i] - $target[$i]) * $delta;
  644. }
  645. // If the assumption is not true, there is a difference between ref2 and ref3.
  646. // We measure this in HSL space. Notation: x' = hsl(x).
  647. $ref2 = _color_rgb2hsl($ref2);
  648. $ref3 = _color_rgb2hsl($ref3);
  649. for ($i = 0; $i < 3; ++$i) {
  650. $shift[$i] = $ref2[$i] - $ref3[$i];
  651. }
  652. // Take the given color, and blend it towards the target.
  653. $given = _color_unpack($given, TRUE);
  654. for ($i = 0; $i < 3; ++$i) {
  655. $result[$i] = $target[$i] + ($given[$i] - $target[$i]) * $delta;
  656. }
  657. // Finally, we apply the extra shift in HSL space.
  658. // Note: if ref2 is a pure blend of ref1 and target, then |shift| = 0.
  659. $result = _color_rgb2hsl($result);
  660. for ($i = 0; $i < 3; ++$i) {
  661. $result[$i] = min(1, max(0, $result[$i] + $shift[$i]));
  662. }
  663. $result = _color_hsl2rgb($result);
  664. // Return hex color.
  665. return _color_pack($result, TRUE);
  666. }
  667. /**
  668. * Converts a hex triplet into a GD color.
  669. */
  670. function _color_gd($img, $hex) {
  671. $c = array_merge([$img], _color_unpack($hex));
  672. return call_user_func_array('imagecolorallocate', $c);
  673. }
  674. /**
  675. * Blends two hex colors and returns the GD color.
  676. */
  677. function _color_blend($img, $hex1, $hex2, $alpha) {
  678. $in1 = _color_unpack($hex1);
  679. $in2 = _color_unpack($hex2);
  680. $out = [$img];
  681. for ($i = 0; $i < 3; ++$i) {
  682. $out[] = $in1[$i] + ($in2[$i] - $in1[$i]) * $alpha;
  683. }
  684. return call_user_func_array('imagecolorallocate', $out);
  685. }
  686. /**
  687. * Converts a hex color into an RGB triplet.
  688. */
  689. function _color_unpack($hex, $normalize = FALSE) {
  690. $hex = substr($hex, 1);
  691. if (strlen($hex) == 3) {
  692. $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
  693. }
  694. $c = hexdec($hex);
  695. for ($i = 16; $i >= 0; $i -= 8) {
  696. $out[] = (($c >> $i) & 0xFF) / ($normalize ? 255 : 1);
  697. }
  698. return $out;
  699. }
  700. /**
  701. * Converts an RGB triplet to a hex color.
  702. */
  703. function _color_pack($rgb, $normalize = FALSE) {
  704. $out = 0;
  705. foreach ($rgb as $k => $v) {
  706. $out |= (($v * ($normalize ? 255 : 1)) << (16 - $k * 8));
  707. }
  708. return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
  709. }
  710. /**
  711. * Converts an HSL triplet into RGB.
  712. */
  713. function _color_hsl2rgb($hsl) {
  714. $h = $hsl[0];
  715. $s = $hsl[1];
  716. $l = $hsl[2];
  717. $m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l * $s;
  718. $m1 = $l * 2 - $m2;
  719. return [
  720. _color_hue2rgb($m1, $m2, $h + 0.33333),
  721. _color_hue2rgb($m1, $m2, $h),
  722. _color_hue2rgb($m1, $m2, $h - 0.33333),
  723. ];
  724. }
  725. /**
  726. * Helper function for _color_hsl2rgb().
  727. */
  728. function _color_hue2rgb($m1, $m2, $h) {
  729. $h = ($h < 0) ? $h + 1 : (($h > 1) ? $h - 1 : $h);
  730. if ($h * 6 < 1) {
  731. return $m1 + ($m2 - $m1) * $h * 6;
  732. }
  733. if ($h * 2 < 1) {
  734. return $m2;
  735. }
  736. if ($h * 3 < 2) {
  737. return $m1 + ($m2 - $m1) * (0.66666 - $h) * 6;
  738. }
  739. return $m1;
  740. }
  741. /**
  742. * Converts an RGB triplet to HSL.
  743. */
  744. function _color_rgb2hsl($rgb) {
  745. $r = $rgb[0];
  746. $g = $rgb[1];
  747. $b = $rgb[2];
  748. $min = min($r, min($g, $b));
  749. $max = max($r, max($g, $b));
  750. $delta = $max - $min;
  751. $l = ($min + $max) / 2;
  752. $s = 0;
  753. if ($l > 0 && $l < 1) {
  754. $s = $delta / ($l < 0.5 ? (2 * $l) : (2 - 2 * $l));
  755. }
  756. $h = 0;
  757. if ($delta > 0) {
  758. if ($max == $r && $max != $g) {
  759. $h += ($g - $b) / $delta;
  760. }
  761. if ($max == $g && $max != $b) {
  762. $h += (2 + ($b - $r) / $delta);
  763. }
  764. if ($max == $b && $max != $r) {
  765. $h += (4 + ($r - $g) / $delta);
  766. }
  767. $h /= 6;
  768. }
  769. return [$h, $s, $l];
  770. }