color.module 22.9 KB
Newer Older
1 2 3
<?php
// $Id$

4
/**
5
 * Implement hook_help().
6 7 8 9
 */
function color_help($path, $arg) {
  switch ($path) {
    case 'admin/help#color':
10 11
      $output = '<p>' . t('The color module allows a site administrator to quickly and easily change the color scheme of certain themes. Although not all themes support color module, both Garland (the default theme) and Minnelli were designed to take advantage of its features. By using color module with a compatible theme, you can easily change the color of links, backgrounds, text, and other theme elements. Color module requires that your <a href="@url">file download method</a> be set to public.', array('@url' => url('admin/settings/file-system'))) . '</p>';
      $output .= '<p>' . t("It is important to remember that color module saves a modified copy of the theme's specified stylesheets in the files directory. This means that if you make any manual changes to your theme's stylesheet, you must save your color settings again, even if they haven't changed. This causes the color module generated version of the stylesheets in the files directory to be recreated using the new version of the original file.") . '</p>';
12
      $output .= '<p>' . t('To change the color settings for a compatible theme, select the "configure" link for the theme on the <a href="@themes">themes administration page</a>.', array('@themes' => url('admin/appearance'))) . '</p>';
13
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@color">Color module</a>.', array('@color' => 'http://drupal.org/handbook/modules/color/')) . '</p>';
14

15 16 17 18
      return $output;
  }
}

19
/**
20
 * Implement hook_theme().
21 22 23 24 25 26 27 28
 */
function color_theme() {
  return array(
    'color_scheme_form' => array(
      'arguments' => array('form' => NULL),
    ),
  );
}
29

30
/**
31
 * Implement hook_form_FORM_ID_alter().
32
 */
33 34
function color_form_system_theme_settings_alter(&$form, &$form_state) {
  if (color_get_info(arg(4)) && function_exists('gd_info')) {
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) != FILE_DOWNLOADS_PUBLIC) {
      // Disables the color changer when the private download method is used.
      // TODO: This should be solved in a different way. See issue #181003.
      drupal_set_message(t('The color picker only works if the <a href="@url">download method</a> is set to public.', array('@url' => url('admin/settings/file-system'))), 'warning');
    }
    else {
      $form['color'] = array(
        '#type' => 'fieldset',
        '#title' => t('Color scheme'),
        '#weight' => -1,
        '#attributes' => array('id' => 'color_scheme_form'),
        '#theme' => 'color_scheme_form',
      );
      $form['color'] += color_scheme_form($form_state, arg(4));
      $form['#submit'][] = 'color_scheme_form_submit';
    }
51
  }
52 53 54
}

/**
55
 * Implement hook_form_FORM_ID_alter().
56 57 58 59
 */
function color_form_system_themes_alter(&$form, &$form_state) {
  _color_theme_select_form_alter($form, $form_state);
}
60

61 62 63 64
/**
 * Helper for hook_form_FORM_ID_alter() implementations.
 */
function _color_theme_select_form_alter(&$form, &$form_state) {
65
  // Use the generated screenshot in the theme list.
66 67
  $themes = list_themes();
  foreach (element_children($form) as $theme) {
68
    if ($screenshot = variable_get('color_' . $theme . '_screenshot')) {
69 70
      if (isset($form[$theme]['screenshot'])) {
        $form[$theme]['screenshot']['#markup'] = theme('image', $screenshot, '', '', array('class' => 'screenshot'), FALSE);
71 72 73 74 75 76 77 78 79
      }
    }
  }
}

/**
 * Callback for the theme to alter the resources used.
 */
function _color_page_alter(&$vars) {
80 81 82
  global $language, $theme_key;

  // Override stylesheets.
83
  $color_paths = variable_get('color_' . $theme_key . '_stylesheets', array());
84 85
  if (!empty($color_paths)) {
    // Loop over theme CSS files and try to rebuild CSS array with rewritten
86
    // stylesheets. Keep the original order intact for CSS cascading.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    $new_theme_css = array();

    foreach ($vars['css']['all']['theme'] as $old_path => $old_preprocess) {
      // Add the non-colored stylesheet first as we might not find a
      // re-colored stylesheet for replacement later.
      $new_theme_css[$old_path] = $old_preprocess;

      // 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 (basename($old_path) == basename($color_path)) {
          // Pull out the non-colored and add rewritten stylesheet.
          unset($new_theme_css[$old_path]);
          $new_theme_css[$color_path] = $old_preprocess;

          // If the current language is RTL and the CSS file had an RTL variant,
          // pull out the non-colored and add rewritten RTL stylesheet.
106
          if ($language->direction == LANGUAGE_RTL) {
107 108 109 110 111 112 113 114 115 116 117 118
            $rtl_old_path = str_replace('.css', '-rtl.css', $old_path);
            $rtl_color_path = str_replace('.css', '-rtl.css', $color_path);
            if (file_exists($rtl_color_path)) {
              unset($new_theme_css[$rtl_old_path]);
              $new_theme_css[$rtl_color_path] = $old_preprocess;
            }
          }
          break;
        }
      }
    }
    $vars['css']['all']['theme'] = $new_theme_css;
119 120 121
    $vars['styles'] = drupal_get_css($vars['css']);
  }

122
  // Override logo.
123
  $logo = variable_get('color_' . $theme_key . '_logo');
124
  if ($logo && $vars['logo'] && preg_match('!' . $theme_key . '/logo.png$!', $vars['logo'])) {
125
    $vars['logo'] = base_path() . $logo;
126 127 128 129 130 131 132 133
  }
}

/**
 * Retrieve the color.module info for a particular theme.
 */
function color_get_info($theme) {
  $path = drupal_get_path('theme', $theme);
134
  $file = DRUPAL_ROOT . '/' . $path . '/color/color.inc';
135 136 137 138 139 140 141 142 143
  if ($path && file_exists($file)) {
    include $file;
    return $info;
  }
}

/**
 * Helper function to retrieve the color palette for a particular theme.
 */
144
function color_get_palette($theme, $default = FALSE) {
145
  // Fetch and expand default palette.
146 147
  $fields = array('base', 'link', 'top', 'bottom', 'text');
  $info = color_get_info($theme);
148 149
  $keys = array_keys($info['schemes']);
  foreach (explode(',', array_shift($keys)) as $k => $scheme) {
150 151 152
    $palette[$fields[$k]] = $scheme;
  }

153
  // Load variable.
154
  return $default ? $palette : variable_get('color_' . $theme . '_palette', $palette);
155 156 157 158 159
}

/**
 * Form callback. Returns the configuration form.
 */
160
function color_scheme_form(&$form_state, $theme) {
161 162 163
  $base = drupal_get_path('module', 'color');
  $info = color_get_info($theme);

164
  // Add Farbtastic color picker.
165
  drupal_add_library('system', 'farbtastic');
166

167
  // Add custom CSS and JS.
168
  drupal_add_css($base . '/color.css', array('preprocess' => FALSE));
169
  drupal_add_js($base . '/color.js');
170
  drupal_add_js(array('color' => array(
171
    'reference' => color_get_palette($theme, TRUE)
172 173
  )), 'setting');

174
  // See if we're using a predefined scheme.
175
  $current = implode(',', variable_get('color_' . $theme . '_palette', array()));
176 177 178
  // Note: we use the original theme when the default scheme is chosen.
  $current = isset($info['schemes'][$current]) ? $current : ($current == '' ? reset($info['schemes']) : '');

179
  // Add scheme selector.
180 181 182 183 184 185 186 187
  $info['schemes'][''] = t('Custom');
  $form['scheme'] = array(
    '#type' => 'select',
    '#title' => t('Color set'),
    '#options' => $info['schemes'],
    '#default_value' => $current,
  );

188
  // Add palette fields.
189 190 191 192 193 194
  $palette = color_get_palette($theme);
  $names = array(
    'base' => t('Base color'),
    'link' => t('Link color'),
    'top' => t('Header top'),
    'bottom' => t('Header bottom'),
195
    'text' => t('Text color'),
196
  );
197
  $form['palette']['#tree'] = TRUE;
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
  foreach ($palette as $name => $value) {
    $form['palette'][$name] = array(
      '#type' => 'textfield',
      '#title' => $names[$name],
      '#default_value' => $value,
      '#size' => 8,
    );
  }
  $form['theme'] = array('#type' => 'value', '#value' => arg(4));
  $form['info'] = array('#type' => 'value', '#value' => $info);

  return $form;
}

/**
213
 * Theme the color form.
214
 *
215
 * @ingroup themeable
216 217 218 219
 */
function theme_color_scheme_form($form) {
  $theme = $form['theme']['#value'];
  $info = $form['info']['#value'];
220
  $path = drupal_get_path('theme', $theme) . '/';
221 222
  drupal_add_css($path . $info['preview_css']);

223
  $output  = '';
224
  $output .= '<div class="color-form clearfix">';
225
  // Color schemes
226 227
  $output .= drupal_render($form['scheme']);
  // Palette
228
  $output .= '<div id="palette" class="clearfix">';
229 230 231 232 233
  foreach (element_children($form['palette']) as $name) {
    $output .= drupal_render($form['palette'][$name]);
  }
  $output .= '</div>';
  // Preview
234
  $output .= drupal_render_children($form);
235 236
  $output .= '<h2>' . t('Preview') . '</h2>';
  $output .= '<div id="preview"><div id="text"><h2>Lorem ipsum dolor</h2><p>Sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud <a href="#">exercitation ullamco</a> laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p></div><div id="img" style="background-image: url(' . base_path() . $path . $info['preview_image'] . ')"></div></div>';
237
  // Close the wrapper div.
238
  $output .= '</div>';
239 240 241 242 243 244 245

  return $output;
}

/**
 * Submit handler for color change form.
 */
246
function color_scheme_form_submit($form, &$form_state) {
247
  // Get theme coloring info.
248
  if (!isset($form_state['values']['info'])) {
249 250
    return;
  }
251 252
  $theme = $form_state['values']['theme'];
  $info = $form_state['values']['info'];
253

254
  // Resolve palette.
255 256 257
  $palette = $form_state['values']['palette'];
  if ($form_state['values']['scheme'] != '') {
    $scheme = explode(',', $form_state['values']['scheme']);
258 259 260 261 262
    foreach ($palette as $k => $color) {
      $palette[$k] = array_shift($scheme);
    }
  }

263 264 265
  // Make sure enough memory is available, if PHP's memory limit is compiled in.
  if (function_exists('memory_get_usage')) {
    // Fetch source image dimensions.
266
    $source = drupal_get_path('theme', $theme) . '/' . $info['base_image'];
267 268 269 270 271 272 273 274
    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;
    $usage = memory_get_usage();
    $limit = parse_size(ini_get('memory_limit'));
    if ($usage + $required > $limit) {
275
      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="@url">PHP documentation</a> for more information.', array('%size' => format_size($usage + $required - $limit), '@url' => 'http://www.php.net/manual/ini.core.php#ini.sect.resource-limits')), 'error');
276 277 278 279
      return;
    }
  }

280
  // Delete old files.
281
  foreach (variable_get('color_' . $theme . '_files', array()) as $file) {
282 283
    @unlink($file);
  }
284
  if (isset($file) && $file = dirname($file)) {
285 286 287 288
    @rmdir($file);
  }

  // Don't render the default colorscheme, use the standard theme instead.
289
  if (implode(',', color_get_palette($theme, TRUE)) == implode(',', $palette)) {
290 291 292 293 294
    variable_del('color_' . $theme . '_palette');
    variable_del('color_' . $theme . '_stylesheets');
    variable_del('color_' . $theme . '_logo');
    variable_del('color_' . $theme . '_files');
    variable_del('color_' . $theme . '_screenshot');
295 296 297
    return;
  }

298
  // Prepare target locations for generated files.
299 300 301
  $id = $theme . '-' . substr(md5(serialize($palette) . microtime()), 0, 8);
  $paths['color'] = file_directory_path() . '/color';
  $paths['target'] = $paths['color'] . '/' . $id;
302
  foreach ($paths as $path) {
303
    file_check_directory($path, FILE_CREATE_DIRECTORY);
304
  }
305
  $paths['target'] = $paths['target'] . '/';
306
  $paths['id'] = $id;
307
  $paths['source'] = drupal_get_path('theme', $theme) . '/';
308 309
  $paths['files'] = $paths['map'] = array();

310
  // Save palette and logo location.
311 312
  variable_set('color_' . $theme . '_palette', $palette);
  variable_set('color_' . $theme . '_logo', $paths['target'] . 'logo.png');
313

314
  // Copy over neutral images.
315 316
  foreach ($info['copy'] as $file) {
    $base = basename($file);
317
    $source = $paths['source'] . $file;
318
    $filepath = file_unmanaged_copy($source, $paths['target'] . $base);
319
    $paths['map'][$file] = $base;
320
    $paths['files'][] = $filepath;
321 322
  }

323
  // Render new images, if image has been provided.
324 325 326
  if ($info['base_image']) {
    _color_render_images($theme, $info, $paths, $palette);
  }
327

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
  // Rewrite theme stylesheets.
  $css = array();
  foreach ($info['css'] as $stylesheet) {
    // Build a temporary array with LTR and RTL files.
    $files = array();
    if (file_exists($paths['source'] . $stylesheet)) {
      $files[] = $stylesheet;

      $rtl_file = str_replace('.css', '-rtl.css', $stylesheet);
      if (file_exists($paths['source'] . $rtl_file)) {
        $files[] = $rtl_file;
      }
    }

    foreach ($files as $file) {
343 344
      // Aggregate @imports recursively for each configured top level CSS file
      // without optimization. Aggregation and optimization will be
345 346 347 348 349
      // handled by drupal_build_css_cache() only.
      $style = drupal_load_stylesheet($paths['source'] . $file, FALSE);

      // Return the path to where this CSS file originated from, stripping
      // off the name of the file at the end of the path.
350
      $base = base_path() . dirname($paths['source'] . $file) . '/';
351 352 353 354 355 356 357 358 359 360
      _drupal_build_css_path(NULL, $base);

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

      // Rewrite stylesheet with new colors.
      $style = _color_rewrite_stylesheet($theme, $info, $paths, $palette, $style);
      $base_file = basename($file);
      $css[] = $paths['target'] . $base_file;
      _color_save_stylesheet($paths['target'] . $base_file, $style, $paths);
361
    }
362
  }
363

364
  // Maintain list of files.
365 366
  variable_set('color_' . $theme . '_stylesheets', $css);
  variable_set('color_' . $theme . '_files', $paths['files']);
367 368 369 370 371
}

/**
 * Rewrite the stylesheet to match the colors in the palette.
 */
372
function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) {
373
  $themes = list_themes();
374
  // Prepare color conversion table.
375 376 377 378 379
  $conversion = $palette;
  unset($conversion['base']);
  foreach ($conversion as $k => $v) {
    $conversion[$k] = drupal_strtolower($v);
  }
380
  $default = color_get_palette($theme, TRUE);
381 382

  // Split off the "Don't touch" section of the stylesheet.
383
  $split = "Color Module: Don't touch";
384
  if (strpos($style, $split) !== FALSE) {
385 386
    list($style, $fixed) = explode($split, $style);
  }
387 388 389

  // 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);
390
  $is_color = FALSE;
391 392 393
  $output = '';
  $base = 'base';

394
  // Iterate over all the parts.
395 396 397
  foreach ($style as $chunk) {
    if ($is_color) {
      $chunk = drupal_strtolower($chunk);
398
      // Check if this is one of the colors in the default palette.
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
      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.
415
      elseif (preg_match('/(?<!-)color[^{:]*:[^{#]*$/i', $chunk)) {
416 417 418 419 420 421 422 423 424 425
        $base = 'text';
      }
      // Reset back to base.
      else {
        $base = 'base';
      }
    }
    $output .= $chunk;
    $is_color = !$is_color;
  }
426 427 428 429
  // Append fixed colors segment.
  if (isset($fixed)) {
    $output .= $fixed;
  }
430

431
  // Replace paths to images.
432
  foreach ($paths['map'] as $before => $after) {
433 434
    $before = base_path() . $paths['source'] . $before;
    $before = preg_replace('`(^|/)(?!../)([^/]+)/../`', '$1', $before);
435 436 437
    $output = str_replace($before, $after, $output);
  }

438
  return $output;
439 440 441
}

/**
442
 * Save the rewritten stylesheet to disk.
443
 */
444
function _color_save_stylesheet($file, $style, &$paths) {
445
  $filepath = file_unmanaged_save_data($style, $file, FILE_EXISTS_REPLACE);
446
  $paths['files'][] = $filepath;
447 448

  // Set standard file permissions for webserver-generated files.
449
  drupal_chmod($file);
450 451 452 453 454 455 456
}

/**
 * Render images that match a given palette.
 */
function _color_render_images($theme, &$info, &$paths, $palette) {

Steven Wittens's avatar
Steven Wittens committed
457
  // Prepare template image.
458
  $source = $paths['source'] . '/' . $info['base_image'];
459 460 461
  $source = imagecreatefrompng($source);
  $width = imagesx($source);
  $height = imagesy($source);
Steven Wittens's avatar
Steven Wittens committed
462 463

  // Prepare target buffer.
464
  $target = imagecreatetruecolor($width, $height);
465
  imagealphablending($target, TRUE);
466 467 468 469 470 471

  // 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]));
  }

Steven Wittens's avatar
Steven Wittens committed
472
  // Render gradient.
473 474
  for ($y = 0; $y < $info['gradient'][3]; ++$y) {
    $color = _color_blend($target, $palette['top'], $palette['bottom'], $y / ($info['gradient'][3] - 1));
475
    imagefilledrectangle($target, $info['gradient'][0], $info['gradient'][1] + $y, $info['gradient'][0] + $info['gradient'][2], $info['gradient'][1] + $y + 1, $color);
476 477
  }

Steven Wittens's avatar
Steven Wittens committed
478
  // Blend over template.
479 480
  imagecopy($target, $source, 0, 0, 0, 0, $width, $height);

Steven Wittens's avatar
Steven Wittens committed
481
  // Clean up template image.
482 483
  imagedestroy($source);

Steven Wittens's avatar
Steven Wittens committed
484
  // Cut out slices.
485 486 487 488 489
  foreach ($info['slices'] as $file => $coord) {
    list($x, $y, $width, $height) = $coord;
    $base = basename($file);
    $image = $paths['target'] . $base;

Steven Wittens's avatar
Steven Wittens committed
490
    // Cut out slice.
491 492 493
    if ($file == 'screenshot.png') {
      $slice = imagecreatetruecolor(150, 90);
      imagecopyresampled($slice, $target, 0, 0, $x, $y, 150, 90, $width, $height);
494
      variable_set('color_' . $theme . '_screenshot', $image);
495 496 497 498 499 500
    }
    else {
      $slice = imagecreatetruecolor($width, $height);
      imagecopy($slice, $target, 0, 0, $x, $y, $width, $height);
    }

Steven Wittens's avatar
Steven Wittens committed
501
    // Save image.
502 503 504 505
    imagepng($slice, $image);
    imagedestroy($slice);
    $paths['files'][] = $image;

506
    // Set standard file permissions for webserver-generated files
507
    drupal_chmod($image);
508

509 510 511 512
    // Build before/after map of image paths.
    $paths['map'][$file] = $base;
  }

Steven Wittens's avatar
Steven Wittens committed
513
  // Clean up target buffer.
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
  imagedestroy($target);
}

/**
 * Shift a given color, using a reference pair and a target blend color.
 *
 * Note: this function is significantly different from the JS version, as it
 * is written to match the blended images perfectly.
 *
 * Constraint: if (ref2 == target + (ref1 - target) * delta) for some fraction delta
 *              then (return == target + (given - target) * delta)
 *
 * Loose constraint: Preserve relative positions in saturation and luminance
 *                   space.
 */
function _color_shift($given, $ref1, $ref2, $target) {
  // We assume that ref2 is a blend of ref1 and target and find
531
  // delta based on the length of the difference vectors.
532 533

  // delta = 1 - |ref2 - ref1| / |white - ref1|
534 535 536
  $target = _color_unpack($target, TRUE);
  $ref1 = _color_unpack($ref1, TRUE);
  $ref2 = _color_unpack($ref2, TRUE);
537 538
  $numerator = 0;
  $denominator = 0;
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
  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.
559
  $given = _color_unpack($given, TRUE);
560 561 562 563 564 565 566 567 568 569 570 571 572
  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.
573
  return _color_pack($result, TRUE);
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
}

/**
 * Convert a hex triplet into a GD color.
 */
function _color_gd($img, $hex) {
  $c = array_merge(array($img), _color_unpack($hex));
  return call_user_func_array('imagecolorallocate', $c);
}

/**
 * Blend two hex colors and return the GD color.
 */
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;
  }
594

595 596 597 598 599 600
  return call_user_func_array('imagecolorallocate', $out);
}

/**
 * Convert a hex color into an RGB triplet.
 */
601
function _color_unpack($hex, $normalize = FALSE) {
602 603 604 605 606 607 608
  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);
  }
609

610 611 612 613 614 615
  return $out;
}

/**
 * Convert an RGB triplet to a hex color.
 */
616
function _color_pack($rgb, $normalize = FALSE) {
617
  $out = 0;
618 619 620
  foreach ($rgb as $k => $v) {
    $out |= (($v * ($normalize ? 255 : 1)) << (16 - $k * 8));
  }
621

622
  return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
623 624 625
}

/**
626
 * Convert a HSL triplet into RGB.
627 628 629 630 631 632 633
 */
function _color_hsl2rgb($hsl) {
  $h = $hsl[0];
  $s = $hsl[1];
  $l = $hsl[2];
  $m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l*$s;
  $m1 = $l * 2 - $m2;
634

635 636 637 638 639
  return array(
    _color_hue2rgb($m1, $m2, $h + 0.33333),
    _color_hue2rgb($m1, $m2, $h),
    _color_hue2rgb($m1, $m2, $h - 0.33333),
  );
640 641 642 643 644 645 646 647 648 649
}

/**
 * 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;
650

651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
  return $m1;
}

/**
 * Convert an RGB triplet to HSL.
 */
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;
666

667
  if ($l > 0 && $l < 1) {
Steven Wittens's avatar
Steven Wittens committed
668
    $s = $delta / ($l < 0.5 ? (2 * $l) : (2 - 2 * $l));
669
  }
670

671 672
  $h = 0;
  if ($delta > 0) {
Steven Wittens's avatar
Steven Wittens committed
673 674 675 676
    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;
677
  }
678

679
  return array($h, $s, $l);
680
}