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