color.module 27.9 KB
Newer Older
1
<?php
2 3 4 5
/**
 * @file
 * Allows users to change the color scheme of themes.
 */
6

7
use Drupal\Component\Utility\Unicode;
8
use Drupal\Core\Asset\CssOptimizer;
9 10
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Environment;
11
use Drupal\Core\Block\BlockPluginInterface;
12
use Drupal\Core\Cache\CacheableMetadata;
13
use Drupal\Core\Form\FormStateInterface;
14
use Drupal\Core\Language\LanguageInterface;
15
use Drupal\Core\Render\Element\Textfield;
16
use Drupal\Core\Routing\RouteMatchInterface;
17

18
/**
19
 * Implements hook_help().
20
 */
21
function color_help($route_name, RouteMatchInterface $route_match) {
22 23
  switch ($route_name) {
    case 'help.page.color':
24
      $output = '<h3>' . t('About') . '</h3>';
25
      $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>.', array(':color_do' => 'https://www.drupal.org/documentation/modules/color')) . '</p>';
26 27
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
28
      $output .= '<dt>' . t('Changing colors') . '</dt>';
29
      $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.', array(':appearance' => \Drupal::url('system.themes_page'))) . '</p>';
30
      $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>';
31
      $output .= '</dl>';
32 33 34 35
      return $output;
  }
}

36
/**
37
 * Implements hook_theme().
38 39 40 41
 */
function color_theme() {
  return array(
    'color_scheme_form' => array(
42
      'render element' => 'form',
43 44 45
    ),
  );
}
46

47
/**
48
 * Implements hook_form_FORM_ID_alter().
49
 */
50
function color_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) {
51 52
  $build_info = $form_state->getBuildInfo();
  if (isset($build_info['args'][0]) && ($theme = $build_info['args'][0]) && color_get_info($theme) && function_exists('gd_info')) {
53
    $form['color'] = array(
54
      '#type' => 'details',
55
      '#title' => t('Color scheme'),
56
      '#open' => TRUE,
57 58 59 60
      '#weight' => -1,
      '#attributes' => array('id' => 'color_scheme_form'),
      '#theme' => 'color_scheme_form',
    );
61
    $form['color'] += color_scheme_form($form, $form_state, $theme);
62
    $form['#validate'][] = 'color_scheme_form_validate';
63 64
    // Ensure color submission happens first so we can unset extra values.
    array_unshift($form['#submit'], 'color_scheme_form_submit');
65
  }
66 67
}

68
/**
69
 * Implements hook_library_info_alter().
70 71 72
 *
 * Replaces style sheets declared in libraries with color-altered style sheets.
 */
73
function color_library_info_alter(&$libraries, $extension) {
74
  $themes = array_keys(\Drupal::service('theme_handler')->listInfo());
75 76
  if (in_array($extension, $themes)) {
    $color_paths = \Drupal::config('color.theme.' . $extension)->get('stylesheets');
77
    if (!empty($color_paths)) {
78
      foreach (array_keys($libraries) as $name) {
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        if(isset($libraries[$name]['css'])) {
          // Override stylesheets.
          foreach ($libraries[$name]['css'] as $category => $css_assets) {
            foreach ($css_assets as $path  => $metadata) {
              // Loop over the path array with recolored CSS files to find matching
              // paths which could replace the non-recolored paths.
              foreach ($color_paths as $color_path) {
                // Color module currently requires unique file names to be used,
                // which allows us to compare different file paths.
                if (drupal_basename($path) == drupal_basename($color_path)) {
                  // Replace the path to the new css file.
                  // This keeps the order of the stylesheets intact.
                  $index = array_search($path, array_keys($libraries[$name]['css'][$category]));
                  $preceding_css_assets = array_slice($libraries[$name]['css'][$category], 0, $index);
                  $succeeding_css_assets = array_slice($libraries[$name]['css'][$category], $index + 1);
                  $libraries[$name]['css'][$category] = array_merge(
                    $preceding_css_assets,
                    [$color_path => $metadata],
                    $succeeding_css_assets
                  );
                }
100 101
              }
            }
102 103 104 105 106 107 108
          }
        }
      }
    }
  }
}

109
/**
110 111 112 113 114 115 116 117
 * Implements hook_block_view_BASE_BLOCK_ID_alter().
 */
function color_block_view_system_branding_block_alter(array &$build, BlockPluginInterface $block) {
  $build['#pre_render'][] = 'color_block_view_pre_render';
}

/**
 * #pre_render callback: Sets color preset logo.
118
 */
119
function color_block_view_pre_render(array $build) {
120
  $theme_key = \Drupal::theme()->getActiveTheme()->getName();
121 122 123 124
  $config = \Drupal::config('color.theme.' . $theme_key);
  CacheableMetadata::createFromRenderArray($build)
    ->addCacheableDependency($config)
    ->applyTo($build);
125

126
  // Override logo.
127
  $logo = $config->get('logo');
128
  if ($logo && $build['content']['site_logo'] && preg_match('!' . $theme_key . '/logo.svg$!', $build['content']['site_logo']['#uri'])) {
129
    $build['content']['site_logo']['#uri'] = file_url_transform_relative(file_create_url($logo));
130
  }
131 132

  return $build;
133 134 135
}

/**
136
 * Retrieves the Color module information for a particular theme.
137 138
 */
function color_get_info($theme) {
139 140 141 142 143 144
  static $theme_info = array();

  if (isset($theme_info[$theme])) {
    return $theme_info[$theme];
  }

145
  $path = drupal_get_path('theme', $theme);
146
  $file = \Drupal::root() . '/' . $path . '/color/color.inc';
147 148
  if ($path && file_exists($file)) {
    include $file;
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    // Add in default values.
    $info += array(
      // CSS files (excluding @import) to rewrite with new color scheme.
      'css' => array(),
      // Files to copy.
      'copy' => array(),
      // Gradient definitions.
      'gradients' => array(),
      // Color areas to fill (x, y, width, height).
      'fill' => array(),
      // Coordinates of all the theme slices (x, y, width, height) with their
      // filename as used in the stylesheet.
      'slices' => array(),
      // Reference color used for blending.
      'blend_target' => '#ffffff',
    );
165
    $theme_info[$theme] = $info;
166 167 168 169 170
    return $info;
  }
}

/**
171
 * Retrieves the color palette for a particular theme.
172
 */
173
function color_get_palette($theme, $default = FALSE) {
174
  // Fetch and expand default palette.
175
  $info = color_get_info($theme);
176
  $palette = $info['schemes']['default']['colors'];
177

178 179 180 181
  if ($default) {
    return $palette;
  }

182
  // Load variable.
183
  // @todo Default color config should be moved to yaml in the theme.
184 185 186 187
  // Getting a mutable override-free object because this function is only used
  // in forms. Color configuration is used to write CSS to the file system
  // making configuration overrides pointless.
  return \Drupal::configFactory()->getEditable('color.theme.' . $theme)->get('palette') ?: $palette;
188 189 190
}

/**
191 192 193 194 195 196 197
 * Form constructor for the color configuration form for a particular theme.
 *
 * @param $theme
 *   The machine name of the theme whose color settings are being configured.
 *
 * @see color_scheme_form_validate()
 * @see color_scheme_form_submit()
198
 */
199
function color_scheme_form($complete_form, FormStateInterface $form_state, $theme) {
200 201
  $info = color_get_info($theme);

202 203 204 205 206 207 208 209 210
  $info['schemes'][''] = array('title' => t('Custom'), 'colors' => array());
  $color_sets = array();
  $schemes = array();
  foreach ($info['schemes'] as $key => $scheme) {
    $color_sets[$key] = $scheme['title'];
    $schemes[$key] = $scheme['colors'];
    $schemes[$key] += $info['schemes']['default']['colors'];
  }

211
  // See if we're using a predefined scheme.
212
  // Note: we use the original theme when the default scheme is chosen.
213 214 215 216 217
  // Note: we use configuration without overrides since this information is used
  // in a form and therefore without doing this would bleed overrides into
  // active configuration. Furthermore, color configuration is used to write
  // CSS to the file system making configuration overrides pointless.
  $current_scheme = \Drupal::configFactory()->getEditable('color.theme.' . $theme)->get('palette');
218 219 220 221 222 223 224 225 226 227 228 229 230 231
  foreach ($schemes as $key => $scheme) {
    if ($current_scheme == $scheme) {
      $scheme_name = $key;
      break;
    }
  }
  if (empty($scheme_name)) {
    if (empty($current_scheme)) {
      $scheme_name = 'default';
    }
    else {
      $scheme_name = '';
    }
  }
232

233
  // Add scheme selector.
234
  $default_palette = color_get_palette($theme, TRUE);
235 236 237
  $form['scheme'] = array(
    '#type' => 'select',
    '#title' => t('Color set'),
238 239
    '#options' => $color_sets,
    '#default_value' => $scheme_name,
240 241
    '#attached' => array(
      'library' => array(
242
        'color/drupal.color',
243
        'color/admin',
244 245
      ),
      // Add custom JavaScript.
246 247
      'drupalSettings' => [
        'color' => [
248
          'reference' => $default_palette,
249 250 251 252
          'schemes' => $schemes,
        ],
        'gradients' => $info['gradients'],
      ],
253
    ),
254 255
  );

256 257
  // Add palette fields. Use the configuration if available.
  $palette = $current_scheme ?: $default_palette;
258
  $names = $info['fields'];
259
  $form['palette']['#tree'] = TRUE;
260
  foreach ($palette as $name => $value) {
261 262 263
    if (isset($names[$name])) {
      $form['palette'][$name] = array(
        '#type' => 'textfield',
264
        '#title' => $names[$name],
265
        '#value_callback' => 'color_palette_color_value',
266 267
        '#default_value' => $value,
        '#size' => 8,
268
        '#attributes' => array('dir' => LanguageInterface::DIRECTION_LTR),
269 270
      );
    }
271
  }
272
  $form['theme'] = array('#type' => 'value', '#value' => $theme);
273 274 275 276
  if (isset($info['#attached'])) {
    $form['#attached'] = $info['#attached'];
    unset($info['#attached']);
  }
277 278 279 280 281 282
  $form['info'] = array('#type' => 'value', '#value' => $info);

  return $form;
}

/**
283
 * Prepares variables for color scheme form templates.
284
 *
285 286 287
 * Default template: color-scheme-form.html.twig.
 *
 * @param array $variables
288 289
 *   An associative array containing:
 *   - form: A render element representing the form.
290
 */
291
function template_preprocess_color_scheme_form(&$variables) {
292
  $form = &$variables['form'];
293

294 295
  $theme = $form['theme']['#value'];
  $info = $form['info']['#value'];
296

297 298
  if (isset($info['preview_library'])) {
    $form['scheme']['#attached']['library'][] = $info['preview_library'];
299
  }
300

301
  // Attempt to load preview HTML if the theme provides it.
302
  $preview_html_path = \Drupal::root() . '/' . (isset($info['preview_html']) ? drupal_get_path('theme', $theme) . '/' . $info['preview_html'] : drupal_get_path('module', 'color') . '/preview.html');
303
  $variables['html_preview']['#markup'] = file_get_contents($preview_html_path);
304 305
}

306 307 308 309 310 311 312 313
/**
 * Determines the value for a palette color field.
 *
 * @param array $element
 *   The form element whose value is being populated.
 * @param string|bool $input
 *   The incoming input to populate the form element. If this is FALSE,
 *   the element's default value should be returned.
314 315
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
316 317
 *
 * @return string
318
 *   The data that will appear in the $form_state->getValues() collection for this
319 320
 *   element. Return nothing to use the default.
 */
321
function color_palette_color_value($element, $input = FALSE, FormStateInterface $form_state) {
322 323 324 325 326 327
  // If we suspect a possible cross-site request forgery attack, only accept
  // hexadecimal CSS color strings from user input, to avoid problems when this
  // value is used in the JavaScript preview.
  if ($input !== FALSE) {
    // Start with the provided value for this textfield, and validate that if
    // necessary, falling back on the default value.
328
    $value = Textfield::valueCallback($element, $input, $form_state);
329 330
    $complete_form = $form_state->getCompleteForm();
    if (!$value || !isset($complete_form['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state->getValue('form_token'), $complete_form['#token'])) {
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
      return $value;
    }
    else {
      return $element['#default_value'];
    }
  }
}

/**
 * Determines if a hexadecimal CSS color string is valid.
 *
 * @param string $color
 *   The string to check.
 *
 * @return bool
 *   TRUE if the string is a valid hexadecimal CSS color string, or FALSE if it
 *   isn't.
 */
function color_valid_hexadecimal_string($color) {
  return preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color);
}

353
/**
354 355 356
 * Form validation handler for color_scheme_form().
 *
 * @see color_scheme_form_submit()
357
 */
358
function color_scheme_form_validate($form, FormStateInterface $form_state) {
359
  // Only accept hexadecimal CSS color strings to avoid XSS upon use.
360
  foreach ($form_state->getValue('palette') as $key => $color) {
361
    if (!color_valid_hexadecimal_string($color)) {
362
      $form_state->setErrorByName('palette][' . $key, t('You must enter a valid hexadecimal color value for %name.', array('%name' => $form['color']['palette'][$key]['#title'])));
363 364 365 366
    }
  }
}

367
/**
368 369 370
 * Form submission handler for color_scheme_form().
 *
 * @see color_scheme_form_validate()
371
 */
372
function color_scheme_form_submit($form, FormStateInterface $form_state) {
373

374 375 376 377 378 379 380 381 382 383
  // Avoid color settings spilling over to theme settings.
  $color_settings = array('theme', 'palette', 'scheme');
  if ($form_state->hasValue('info')) {
    $color_settings[] = 'info';
  }
  foreach ($color_settings as $setting_name) {
    ${$setting_name} = $form_state->getValue($setting_name);
    $form_state->unsetValue($setting_name);
  }
  if (!isset($info)) {
384 385 386
    return;
  }

387
  $config = \Drupal::configFactory()->getEditable('color.theme.' . $theme);
388

389
  // Resolve palette.
390
  if ($scheme != '') {
391
    foreach ($palette as $key => $color) {
392 393
      if (isset($info['schemes'][$scheme]['colors'][$key])) {
        $palette[$key] = $info['schemes'][$scheme]['colors'][$key];
394
      }
395
    }
396
    $palette += $info['schemes']['default']['colors'];
397 398
  }

399
  // Make sure enough memory is available.
400 401 402 403 404 405 406 407 408 409 410 411 412 413
  if (isset($info['base_image'])) {
    // Fetch source image dimensions.
    $source = drupal_get_path('theme', $theme) . '/' . $info['base_image'];
    list($width, $height) = getimagesize($source);

    // We need at least a copy of the source and a target buffer of the same
    // size (both at 32bpp).
    $required = $width * $height * 8;
    // We intend to prevent color scheme changes if there isn't enough memory
    // available.  memory_get_usage(TRUE) returns a more accurate number than
    // memory_get_usage(), therefore we won't inadvertently reject a color
    // scheme change based on a faulty memory calculation.
    $usage = memory_get_usage(TRUE);
    $memory_limit = ini_get('memory_limit');
414 415
    $size = Bytes::toInt($memory_limit);
    if (!Environment::checkMemoryLimit($usage + $required, $memory_limit)) {
416
      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.', array('%size' => format_size($usage + $required - $size))), 'error');
417 418
      return;
    }
419 420
  }

421
  // Delete old files.
422 423 424 425 426
  $files = $config->get('files');
  if (isset($files)) {
    foreach ($files as $file) {
      @drupal_unlink($file);
    }
427
  }
428
  if (isset($file) && $file = dirname($file)) {
429
    @drupal_rmdir($file);
430 431
  }

432
  // No change in color config, use the standard theme from color.inc.
433
  if (implode(',', color_get_palette($theme, TRUE)) == implode(',', $palette)) {
434
    $config->delete();
435 436 437
    return;
  }

438
  // Prepare target locations for generated files.
439
  $id = $theme . '-' . substr(hash('sha256', serialize($palette) . microtime()), 0, 8);
440
  $paths['color'] = 'public://color';
441
  $paths['target'] = $paths['color'] . '/' . $id;
442
  foreach ($paths as $path) {
443
    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
444
  }
445
  $paths['target'] = $paths['target'] . '/';
446
  $paths['id'] = $id;
447
  $paths['source'] = drupal_get_path('theme', $theme) . '/';
448 449
  $paths['files'] = $paths['map'] = array();

450
  // Save palette and logo location.
451 452
  $config
    ->set('palette', $palette)
453
    ->set('logo', $paths['target'] . 'logo.svg')
454
    ->save();
455

456
  // Copy over neutral images.
457
  foreach ($info['copy'] as $file) {
458
    $base = drupal_basename($file);
459
    $source = $paths['source'] . $file;
460
    $filepath = file_unmanaged_copy($source, $paths['target'] . $base);
461
    $paths['map'][$file] = $base;
462
    $paths['files'][] = $filepath;
463 464
  }

465
  // Render new images, if image has been provided.
466
  if (isset($info['base_image'])) {
467 468
    _color_render_images($theme, $info, $paths, $palette);
  }
469

470 471 472
  // Rewrite theme stylesheets.
  $css = array();
  foreach ($info['css'] as $stylesheet) {
473
    // Build a temporary array with CSS files.
474 475 476 477 478 479
    $files = array();
    if (file_exists($paths['source'] . $stylesheet)) {
      $files[] = $stylesheet;
    }

    foreach ($files as $file) {
480
      $css_optimizer = new CssOptimizer();
481 482
      // Aggregate @imports recursively for each configured top level CSS file
      // without optimization. Aggregation and optimization will be
483
      // handled by drupal_build_css_cache() only.
484
      $style = $css_optimizer->loadFile($paths['source'] . $file, FALSE);
485 486 487

      // Return the path to where this CSS file originated from, stripping
      // off the name of the file at the end of the path.
488
      $css_optimizer->rewriteFileURIBasePath = base_path() . dirname($paths['source'] . $file) . '/';
489 490

      // Prefix all paths within this CSS file, ignoring absolute paths.
491
      $style = preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', array($css_optimizer, 'rewriteFileURI'), $style);
492 493 494

      // Rewrite stylesheet with new colors.
      $style = _color_rewrite_stylesheet($theme, $info, $paths, $palette, $style);
495
      $base_file = drupal_basename($file);
496 497
      $css[] = $paths['target'] . $base_file;
      _color_save_stylesheet($paths['target'] . $base_file, $style, $paths);
498
    }
499
  }
500

501
  // Maintain list of files.
502 503 504 505
  $config
    ->set('stylesheets', $css)
    ->set('files', $paths['files'])
    ->save();
506 507 508
}

/**
509
 * Rewrites the stylesheet to match the colors in the palette.
510
 */
511
function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) {
512
  // Prepare color conversion table.
513 514
  $conversion = $palette;
  foreach ($conversion as $k => $v) {
515
    $conversion[$k] = Unicode::strtolower($v);
516
  }
517
  $default = color_get_palette($theme, TRUE);
518 519

  // Split off the "Don't touch" section of the stylesheet.
520
  $split = "Color Module: Don't touch";
521
  if (strpos($style, $split) !== FALSE) {
522 523
    list($style, $fixed) = explode($split, $style);
  }
524 525 526

  // Find all colors in the stylesheet and the chunks in between.
  $style = preg_split('/(#[0-9a-f]{6}|#[0-9a-f]{3})/i', $style, -1, PREG_SPLIT_DELIM_CAPTURE);
527
  $is_color = FALSE;
528 529 530
  $output = '';
  $base = 'base';

531
  // Iterate over all the parts.
532 533
  foreach ($style as $chunk) {
    if ($is_color) {
534
      $chunk = Unicode::strtolower($chunk);
535
      // Check if this is one of the colors in the default palette.
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
      if ($key = array_search($chunk, $default)) {
        $chunk = $conversion[$key];
      }
      // Not a pre-set color. Extrapolate from the base.
      else {
        $chunk = _color_shift($palette[$base], $default[$base], $chunk, $info['blend_target']);
      }
    }
    else {
      // Determine the most suitable base color for the next color.

      // 'a' declarations. Use link.
      if (preg_match('@[^a-z0-9_-](a)[^a-z0-9_-][^/{]*{[^{]+$@i', $chunk)) {
        $base = 'link';
      }
      // 'color:' styles. Use text.
552
      elseif (preg_match('/(?<!-)color[^{:]*:[^{#]*$/i', $chunk)) {
553 554 555 556 557 558 559 560 561 562
        $base = 'text';
      }
      // Reset back to base.
      else {
        $base = 'base';
      }
    }
    $output .= $chunk;
    $is_color = !$is_color;
  }
563 564 565 566
  // Append fixed colors segment.
  if (isset($fixed)) {
    $output .= $fixed;
  }
567

568
  // Replace paths to images.
569
  foreach ($paths['map'] as $before => $after) {
570 571
    $before = base_path() . $paths['source'] . $before;
    $before = preg_replace('`(^|/)(?!../)([^/]+)/../`', '$1', $before);
572 573 574
    $output = str_replace($before, $after, $output);
  }

575
  return $output;
576 577 578
}

/**
579
 * Saves the rewritten stylesheet to disk.
580
 */
581
function _color_save_stylesheet($file, $style, &$paths) {
582
  $filepath = file_unmanaged_save_data($style, $file, FILE_EXISTS_REPLACE);
583
  $paths['files'][] = $filepath;
584 585

  // Set standard file permissions for webserver-generated files.
586
  drupal_chmod($file);
587 588 589
}

/**
590
 * Renders images that match a given palette.
591 592
 */
function _color_render_images($theme, &$info, &$paths, $palette) {
Steven Wittens's avatar
Steven Wittens committed
593
  // Prepare template image.
594
  $source = $paths['source'] . '/' . $info['base_image'];
595 596 597
  $source = imagecreatefrompng($source);
  $width = imagesx($source);
  $height = imagesy($source);
Steven Wittens's avatar
Steven Wittens committed
598 599

  // Prepare target buffer.
600
  $target = imagecreatetruecolor($width, $height);
601
  imagealphablending($target, TRUE);
602 603 604 605 606 607

  // Fill regions of solid color.
  foreach ($info['fill'] as $color => $fill) {
    imagefilledrectangle($target, $fill[0], $fill[1], $fill[0] + $fill[2], $fill[1] + $fill[3], _color_gd($target, $palette[$color]));
  }

608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
  // Render gradients.
  foreach ($info['gradients'] as $gradient) {
    // Get direction of the gradient.
    if (isset($gradient['direction']) && $gradient['direction'] == 'horizontal') {
      // Horizontal gradient.
      for ($x = 0; $x < $gradient['dimension'][2]; $x++) {
        $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $x / ($gradient['dimension'][2] - 1));
        imagefilledrectangle($target, ($gradient['dimension'][0] + $x), $gradient['dimension'][1], ($gradient['dimension'][0] + $x + 1), ($gradient['dimension'][1] + $gradient['dimension'][3]), $color);
      }
    }
    else {
      // Vertical gradient.
      for ($y = 0; $y < $gradient['dimension'][3]; $y++) {
        $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $y / ($gradient['dimension'][3] - 1));
        imagefilledrectangle($target, $gradient['dimension'][0], $gradient['dimension'][1] + $y, $gradient['dimension'][0] + $gradient['dimension'][2], $gradient['dimension'][1] + $y + 1, $color);
      }
    }
625 626
  }

Steven Wittens's avatar
Steven Wittens committed
627
  // Blend over template.
628 629
  imagecopy($target, $source, 0, 0, 0, 0, $width, $height);

Steven Wittens's avatar
Steven Wittens committed
630
  // Clean up template image.
631 632
  imagedestroy($source);

Steven Wittens's avatar
Steven Wittens committed
633
  // Cut out slices.
634 635
  foreach ($info['slices'] as $file => $coord) {
    list($x, $y, $width, $height) = $coord;
636
    $base = drupal_basename($file);
637
    $image = drupal_realpath($paths['target'] . $base);
638

Steven Wittens's avatar
Steven Wittens committed
639
    // Cut out slice.
640 641 642
    if ($file == 'screenshot.png') {
      $slice = imagecreatetruecolor(150, 90);
      imagecopyresampled($slice, $target, 0, 0, $x, $y, 150, 90, $width, $height);
643
      \Drupal::configFactory()->getEditable('color.theme.' . $theme)
644 645
        ->set('screenshot', $image)
        ->save();
646 647 648 649 650 651
    }
    else {
      $slice = imagecreatetruecolor($width, $height);
      imagecopy($slice, $target, 0, 0, $x, $y, $width, $height);
    }

Steven Wittens's avatar
Steven Wittens committed
652
    // Save image.
653 654 655 656
    imagepng($slice, $image);
    imagedestroy($slice);
    $paths['files'][] = $image;

657
    // Set standard file permissions for webserver-generated files
658
    drupal_chmod($image);
659

660 661 662 663
    // Build before/after map of image paths.
    $paths['map'][$file] = $base;
  }

Steven Wittens's avatar
Steven Wittens committed
664
  // Clean up target buffer.
665 666 667 668
  imagedestroy($target);
}

/**
669
 * Shifts a given color, using a reference pair and a target blend color.
670 671 672 673
 *
 * Note: this function is significantly different from the JS version, as it
 * is written to match the blended images perfectly.
 *
674 675
 * Constraint: if (ref2 == target + (ref1 - target) * delta) for some fraction
 * delta then (return == target + (given - target) * delta).
676 677
 *
 * Loose constraint: Preserve relative positions in saturation and luminance
678
 * space.
679 680 681
 */
function _color_shift($given, $ref1, $ref2, $target) {
  // We assume that ref2 is a blend of ref1 and target and find
682
  // delta based on the length of the difference vectors.
683 684

  // delta = 1 - |ref2 - ref1| / |white - ref1|
685 686 687
  $target = _color_unpack($target, TRUE);
  $ref1 = _color_unpack($ref1, TRUE);
  $ref2 = _color_unpack($ref2, TRUE);
688 689
  $numerator = 0;
  $denominator = 0;
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
  for ($i = 0; $i < 3; ++$i) {
    $numerator += ($ref2[$i] - $ref1[$i]) * ($ref2[$i] - $ref1[$i]);
    $denominator += ($target[$i] - $ref1[$i]) * ($target[$i] - $ref1[$i]);
  }
  $delta = ($denominator > 0) ? (1 - sqrt($numerator / $denominator)) : 0;

  // Calculate the color that ref2 would be if the assumption was true.
  for ($i = 0; $i < 3; ++$i) {
    $ref3[$i] = $target[$i] + ($ref1[$i] - $target[$i]) * $delta;
  }

  // If the assumption is not true, there is a difference between ref2 and ref3.
  // We measure this in HSL space. Notation: x' = hsl(x).
  $ref2 = _color_rgb2hsl($ref2);
  $ref3 = _color_rgb2hsl($ref3);
  for ($i = 0; $i < 3; ++$i) {
    $shift[$i] = $ref2[$i] - $ref3[$i];
  }

  // Take the given color, and blend it towards the target.
710
  $given = _color_unpack($given, TRUE);
711 712 713 714 715 716 717 718 719 720 721 722 723
  for ($i = 0; $i < 3; ++$i) {
    $result[$i] = $target[$i] + ($given[$i] - $target[$i]) * $delta;
  }

  // Finally, we apply the extra shift in HSL space.
  // Note: if ref2 is a pure blend of ref1 and target, then |shift| = 0.
  $result = _color_rgb2hsl($result);
  for ($i = 0; $i < 3; ++$i) {
    $result[$i] = min(1, max(0, $result[$i] + $shift[$i]));
  }
  $result = _color_hsl2rgb($result);

  // Return hex color.
724
  return _color_pack($result, TRUE);
725 726 727
}

/**
728
 * Converts a hex triplet into a GD color.
729 730 731 732 733 734 735
 */
function _color_gd($img, $hex) {
  $c = array_merge(array($img), _color_unpack($hex));
  return call_user_func_array('imagecolorallocate', $c);
}

/**
736
 * Blends two hex colors and returns the GD color.
737 738 739 740 741 742 743 744
 */
function _color_blend($img, $hex1, $hex2, $alpha) {
  $in1 = _color_unpack($hex1);
  $in2 = _color_unpack($hex2);
  $out = array($img);
  for ($i = 0; $i < 3; ++$i) {
    $out[] = $in1[$i] + ($in2[$i] - $in1[$i]) * $alpha;
  }
745

746 747 748 749
  return call_user_func_array('imagecolorallocate', $out);
}

/**
750
 * Converts a hex color into an RGB triplet.
751
 */
752
function _color_unpack($hex, $normalize = FALSE) {
753 754 755 756 757 758 759
  if (strlen($hex) == 4) {
    $hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
  }
  $c = hexdec($hex);
  for ($i = 16; $i >= 0; $i -= 8) {
    $out[] = (($c >> $i) & 0xFF) / ($normalize ? 255 : 1);
  }
760

761 762 763 764
  return $out;
}

/**
765
 * Converts an RGB triplet to a hex color.
766
 */
767
function _color_pack($rgb, $normalize = FALSE) {
768
  $out = 0;
769 770 771
  foreach ($rgb as $k => $v) {
    $out |= (($v * ($normalize ? 255 : 1)) << (16 - $k * 8));
  }
772

773
  return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
774 775 776
}

/**
777
 * Converts an HSL triplet into RGB.
778 779 780 781 782
 */
function _color_hsl2rgb($hsl) {
  $h = $hsl[0];
  $s = $hsl[1];
  $l = $hsl[2];
783
  $m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l * $s;
784
  $m1 = $l * 2 - $m2;
785

786 787 788 789 790
  return array(
    _color_hue2rgb($m1, $m2, $h + 0.33333),
    _color_hue2rgb($m1, $m2, $h),
    _color_hue2rgb($m1, $m2, $h - 0.33333),
  );
791 792 793 794 795 796 797 798 799 800
}

/**
 * Helper function for _color_hsl2rgb().
 */
function _color_hue2rgb($m1, $m2, $h) {
  $h = ($h < 0) ? $h + 1 : (($h > 1) ? $h - 1 : $h);
  if ($h * 6 < 1) return $m1 + ($m2 - $m1) * $h * 6;
  if ($h * 2 < 1) return $m2;
  if ($h * 3 < 2) return $m1 + ($m2 - $m1) * (0.66666 - $h) * 6;
801

802 803 804 805
  return $m1;
}

/**
806
 * Converts an RGB triplet to HSL.
807 808 809 810 811 812 813 814 815 816
 */
function _color_rgb2hsl($rgb) {
  $r = $rgb[0];
  $g = $rgb[1];
  $b = $rgb[2];
  $min = min($r, min($g, $b));
  $max = max($r, max($g, $b));
  $delta = $max - $min;
  $l = ($min + $max) / 2;
  $s = 0;
817

818
  if ($l > 0 && $l < 1) {
Steven Wittens's avatar
Steven Wittens committed
819
    $s = $delta / ($l < 0.5 ? (2 * $l) : (2 - 2 * $l));
820
  }
821

822 823
  $h = 0;
  if ($delta > 0) {
Steven Wittens's avatar
Steven Wittens committed
824 825 826 827
    if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
    if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
    if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
    $h /= 6;
828
  }
829

830
  return array($h, $s, $l);
831
}