<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    themewizard
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__themewizard()
{
    global $THEME_WIZARD_IMAGES_CACHE, $THEME_SEED_CACHE, $THEME_DARK_CACHE;
    $THEME_WIZARD_IMAGES_CACHE = array();
    $THEME_SEED_CACHE = array();
    $THEME_DARK_CACHE = array();

    global $THEME_WIZARD_IMAGES, $THEME_WIZARD_IMAGES_NO_WILD;
    $THEME_WIZARD_IMAGES = array();
    $THEME_WIZARD_IMAGES_NO_WILD = array();
    if (function_exists('imagecreatefromgif')) {
        $THEME_WIZARD_IMAGES[] = '';
    }

    $hooks = find_all_hooks('modules', 'admin_themewizard');
    foreach (array_keys($hooks) as $hook) {
        require_code('hooks/modules/admin_themewizard/' . filter_naughty_harsh($hook));
        $ob = object_factory('Hook_admin_themewizard_' . filter_naughty_harsh($hook), true);
        if (is_null($ob)) {
            continue;
        }
        $results = $ob->run();
        if (is_null($results)) {
            continue;
        }
        list($a, $b) = $results;
        $THEME_WIZARD_IMAGES = array_merge($THEME_WIZARD_IMAGES, $a);
        $THEME_WIZARD_IMAGES_NO_WILD = array_merge($THEME_WIZARD_IMAGES_NO_WILD, $b);
    }

    require_code('images');
}

/**
 * Given a source theme name, configure the theme wizard for theme generation from it.
 *
 * @param  ID_TEXT $theme The theme name
 * @param  boolean $guess_images_if_needed Whether we suspect the theme might not be well defined
 */
function load_themewizard_params_from_theme($theme, $guess_images_if_needed = false)
{
    global $THEME_WIZARD_IMAGES_CACHE;
    if (isset($THEME_WIZARD_IMAGES_CACHE[$theme])) {
        return;
    }

    require_code('files');

    $map = array();
    if ($theme != 'default') {
        $ini_path = get_custom_file_base() . '/themes/' . filter_naughty($theme) . '/theme.ini';
        if (file_exists($ini_path)) {
            $map += better_parse_ini_file($ini_path);
        }
    }
    $ini_path = get_file_base() . '/themes/default/theme.ini';
    $autodetect_background_images = $guess_images_if_needed && (!isset($map['theme_wizard_images']));
    $map += better_parse_ini_file($ini_path); // NB: Does not take precedence

    if (!isset($map['theme_wizard_images'])) {
        $map['theme_wizard_images'] = '';
    }
    if (!isset($map['theme_wizard_images_no_wild'])) {
        $map['theme_wizard_images_no_wild'] = '';
    }

    if ($autodetect_background_images) {
        $css_dir_path = get_custom_file_base() . '/themes/' . filter_naughty($theme) . (($theme == 'default') ? '/css/' : '/css_custom/');
        if (!is_dir($css_dir_path)) {
            $css_dir_path = get_file_base() . '/themes/' . filter_naughty($theme) . (($theme == 'default') ? '/css/' : '/css_custom/');
        }
        $dh = opendir($css_dir_path);
        while (($sheet = readdir($dh)) !== false) {
            if (substr($sheet, -4) == '.css') {
                $css_path = get_custom_file_base() . '/themes/' . filter_naughty($theme) . '/css_custom/' . $sheet;
                if (!file_exists($css_path)) {
                    $css_path = get_custom_file_base() . '/themes/default/css_custom/' . $sheet;
                }
                if (!file_exists($css_path)) {
                    $css_path = get_file_base() . '/themes/default/css/' . $sheet;
                }
                $css_file = file_get_contents($css_path);
                $matches = array();
                $num_matches = preg_match_all('#\{\$IMG[;\#]?,([\w\-]+)\}#', $css_file, $matches);
                for ($i = 0; $i < $num_matches; $i++) {
                    if ((preg_match('#' . preg_quote($matches[0][$i]) . '[\'"]?\)[^\n]*no-repeat#', $css_file) == 0) || (preg_match('#' . preg_quote($matches[0][$i]) . '[\'"]?\)[^\n]*width:\s*\d\d\d+px#', $css_file) != 0) || (preg_match('#width:\s*\d\d\d+px;[^\n]*' . preg_quote($matches[0][$i]) . '[\'"]?\)#', $css_file) != 0)) {
                        $map['theme_wizard_images'] .= ',' . $matches[1][$i];
                    }
                }
            }
        }

        if ($theme != 'default') {
            $myfile = fopen(get_custom_file_base() . '/themes/' . filter_naughty($theme) . '/theme.ini', 'at');
            flock($myfile, LOCK_EX);
            fseek($myfile, 0, SEEK_END);
            fwrite($myfile, 'theme_wizard_images=' . $map['theme_wizard_images'] . "\n");
            flock($myfile, LOCK_UN);
            fclose($myfile);
        }
    }

    global $THEME_WIZARD_IMAGES, $THEME_WIZARD_IMAGES_NO_WILD;
    $THEME_WIZARD_IMAGES = explode(',', $map['theme_wizard_images']);
    $THEME_WIZARD_IMAGES_NO_WILD = explode(',', $map['theme_wizard_images_no_wild']);

    // Remove gifs if we do not support them
    if (!function_exists('imagecreatefromgif')) {
        global $THEME_IMAGES_CACHE;

        $temporary_default = isset($_GET['keep_theme_seed']);
        if ($temporary_default) { // To stop an infinite loop, with find_theme_image trying to load up the theme wizard subsystem
            $tseed = $_GET['keep_theme_seed'];
            unset($_GET['keep_theme_seed']);
            $img_codes_bak = $THEME_IMAGES_CACHE;
        }
        $new = array();
        foreach ($THEME_WIZARD_IMAGES as $theme_image) {
            if (substr(find_theme_image($theme_image, true, false, $theme), -4) != '.gif') {
                $new[] = $theme_image;
            }
        }
        $THEME_WIZARD_IMAGES = $new;
        if ($temporary_default) {
            $_GET['keep_theme_seed'] = $tseed;
            $THEME_IMAGES_CACHE = $img_codes_bak;
        }
    }

    $THEME_WIZARD_IMAGES_CACHE[$theme] = $THEME_WIZARD_IMAGES;
}

/**
 * Find the seed of a theme.
 *
 * @param  ID_TEXT $theme The theme name
 * @param  boolean $no_easy_anchor Whether we can't assume the theme has any Composr default colour information defined, if not in theme.ini
 * @return ID_TEXT The seed colour
 */
function find_theme_seed($theme, $no_easy_anchor = false)
{
    global $THEME_SEED_CACHE;
    if (isset($THEME_SEED_CACHE[$theme])) {
        return $THEME_SEED_CACHE[$theme];
    }

    $ini_path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . filter_naughty($theme) . '/theme.ini';
    if (is_file($ini_path)) {
        require_code('files');
        $map = better_parse_ini_file($ini_path);
    } else {
        $map = array();
    }

    if (!array_key_exists('seed', $map)) {
        $css_path = get_custom_file_base() . '/themes/' . $theme . '/css_custom/global.css';
        if (!is_file($css_path)) {
            $css_path = get_file_base() . '/themes/default/css/global.css';
        }
        $css_file_contents = file_get_contents($css_path);
        $matches = array();
        if (preg_match('#\{\$THEME\_WIZARD\_COLOR,\#(.{6}),seed,.*\}#', $css_file_contents, $matches) != 0) {
            $THEME_SEED_CACHE[$theme] = $matches[1];
        } else {
            /*if ($no_easy_anchor)
            {
                   We could put some auto-detection code here; possibly a future improvement but not needed currently.
            } else {*/
            if ($theme == 'default') {
                fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
            }
            $THEME_SEED_CACHE[$theme] = find_theme_seed('default');
            //}
        }
    } else {
        $THEME_SEED_CACHE[$theme] = $map['seed'];
    }

    return $THEME_SEED_CACHE[$theme];
}

/**
 * Find whether a theme is dark.
 *
 * @param  ID_TEXT $theme The theme name
 * @return boolean Whether the theme is dark
 */
function find_theme_dark($theme)
{
    global $THEME_DARK_CACHE;
    if (isset($THEME_DARK_CACHE[$theme])) {
        return $THEME_DARK_CACHE[$theme];
    }

    $css_path = get_custom_file_base() . '/themes/' . $theme . '/css_custom/global.css';
    if (!is_file($css_path)) {
        $css_path = get_file_base() . '/themes/default/css/global.css';
    }
    if (!is_file($css_path)) {
        return false;
    }
    $css_file_contents = file_get_contents($css_path);
    $matches = array();
    if (preg_match('#\{\$THEME\_WIZARD\_COLOR,\#(.{6}),WB,.*\}#', $css_file_contents, $matches) != 0) {
        $THEME_DARK_CACHE[$theme] = (strtoupper($matches[1]) != 'FFFFFF');
    } else {
        $THEME_DARK_CACHE[$theme] = false;
    }

    return $THEME_DARK_CACHE[$theme];
}

/**
 * Called by find_theme_image to allow on-the-fly previewing of what theme wizard output would look like.
 *
 * @param  ID_TEXT $id The theme image ID
 * @param  boolean $silent_fail Whether to silently fail (i.e. not give out an error message when a theme image cannot be found)
 * @return ?URLPATH URL to image (null: use standard one, this one is not theme wizard influenced).
 */
function find_theme_image_themewizard_preview($id, $silent_fail = false)
{
    load_themewizard_params_from_theme(get_param_string('keep_theme_source', 'default'), get_param_string('keep_theme_algorithm', 'equations') == 'hsv');

    $seed = get_param_string('keep_theme_seed');
    if ($seed == 'random') {
        $_GET['keep_theme_seed'] = str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT);
        require_lang('themes');
        attach_message(do_lang_tempcode('SEED_IS', escape_html($_GET['keep_theme_seed'])), 'inform');
    }

    $tseed = $_GET['keep_theme_seed'];
    unset($_GET['keep_theme_seed']);
    $test = find_theme_image($id, $silent_fail);
    $_GET['keep_theme_seed'] = $tseed;
    if ($test == '') {
        return null;
    }

    global $THEME_WIZARD_IMAGES, $THEME_WIZARD_IMAGES_NO_WILD;
    if (!in_array($id, $THEME_WIZARD_IMAGES_NO_WILD)) {
        foreach ($THEME_WIZARD_IMAGES as $expression) {
            if (($expression == $id) || ((substr($expression, -1) == '*') && (substr($id, 0, strlen($expression) - 1) . '*' == $expression))) {
                $keep = keep_symbol(array());
                return find_script('themewizard') . '?type=image&show=' . urlencode($id) . $keep;
            }
        }
    }

    return null;
}

/**
 * Generate a logo from the template.
 *
 * @param  string $name The site name.
 * @param  ?string $font_choice The font name (in data/fonts) (null: default).
 * @param  string $logo_theme_image The logo theme image.
 * @param  string $background_theme_image The background theme image.
 * @param  boolean $raw Whether to output the logo to the browser, destroy then image, and exit the script (i.e. never returns)
 * @param  ?string $theme The theme to use the logo template from (null: default root zone theme).
 * @param  boolean $standalone_version Whether we are generating the standalone version (smaller, used in e-mails etc).
 * @return resource The image resource.
 */
function generate_logo($name, $font_choice = null, $logo_theme_image = 'logo/default_logos/1', $background_theme_image = 'logo/default_backgrounds/banner1', $raw = false, $theme = null, $standalone_version = false)
{
    require_code('character_sets');
    require_code('files');
    require_code('themes2');

    if (is_null($theme)) {
        $theme = $GLOBALS['SITE_DB']->query_select_value('zones', 'zone_theme', array('zone_name' => ''));
        if (($theme == '') || ($theme == '-1')) {
            $theme = 'default';
        }
    }

    // Load up details
    $logowizard_details = array();
    if ($theme != 'default') {
        $ini_path = get_custom_file_base() . '/themes/' . filter_naughty($theme) . '/theme.ini';
        if (file_exists($ini_path)) {
            $logowizard_details += better_parse_ini_file($ini_path);
        }
    }
    $ini_path = get_file_base() . '/themes/default/theme.ini';
    $logowizard_details += better_parse_ini_file($ini_path);

    // Load background image
    $imgs = array();
    foreach (array('logo' => $logo_theme_image, 'background' => $background_theme_image, 'standalone' => 'logo/standalone_logo') as $id => $theme_image) {
        $url = find_theme_image($theme_image, false, false, $theme, null, null, true);
        $file_path_stub = convert_url_to_path($url);
        if (!is_null($file_path_stub)) {
            if (!file_exists($file_path_stub)) {
                $file_path_stub = get_file_base() . '/themes/default/images/EN/logo/' . filter_naughty($theme_image) . '.png'; // Exceptional situation. Maybe theme got corrupted?
            }
            $data = file_get_contents($file_path_stub);
        } else {
            $data = http_download_file($url);
        }
        $img = cms_imagecreatefromstring($data, get_file_extension($url));
        if ($img === false) {
            warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($url)));
        }
        $imgs[$id] = $img;
    }
    if ($standalone_version) {
        // Based on 'background' image, but must be the size of 'standalone' image...

        $canvas = imagecreatetruecolor(imagesx($imgs['standalone']), imagesy($imgs['standalone']));

        imagealphablending($canvas, false);
        $transparent = imagecolortransparent($imgs['background']);
        if ($transparent >= imagecolorstotal($imgs['background'])) { // Workaround for corrupt images
            $transparent = -1;
        }
        if ($transparent != -1) {
            $_transparent = imagecolorsforindex($imgs['background'], $transparent);
            imagecolortransparent($canvas, imagecolorallocate($canvas, $_transparent['red'], $_transparent['green'], $_transparent['blue']));
        }

        imagecopy($canvas, $imgs['background'], 0, 0, 0, 0, imagesx($imgs['standalone']), imagesy($imgs['standalone']));

        imagedestroy($imgs['background']);
        imagedestroy($imgs['standalone']);
    } else {
        $canvas = $imgs['background'];
        imagedestroy($imgs['standalone']);
    }
    imagealphablending($canvas, true);

    // Add logo onto the canvas
    imagecopy($canvas, $imgs['logo'], intval($logowizard_details['logo_x_offset']), intval($logowizard_details['logo_y_offset']), 0, 0, imagesx($imgs['logo']), imagesy($imgs['logo']));
    imagedestroy($imgs['logo']);

    // Find font details
    require_code('fonts');
    $font_path = find_font_path($font_choice);
    if (!has_ttf()) {
        $font = intval($logowizard_details['site_name_font_size_small_non_ttf']);
        $font_width = imagefontwidth($font) * strlen($name);
        $font_height = imagefontheight($font);
    } else {
        list(, , $font_width, , , , , $font_height) = imagettfbbox(26.0, 0.0, $font_path, foxy_utf8_to_nce($name));
        $font_height = max($font_height, -$font_height);
    }

    // Declare colours
    $white = imagecolorallocate($canvas, hexdec(substr($logowizard_details['site_name_colour'], 0, 2)), hexdec(substr($logowizard_details['site_name_colour'], 2, 2)), hexdec(substr($logowizard_details['site_name_colour'], 4, 2)));
    $black = imagecolorallocate($canvas, 0, 0, 0);
    if (file_exists(get_custom_file_base() . '/themes/' . $theme . '/css_custom/global.css')) {
        $css_file = file_get_contents(get_custom_file_base() . '/themes/' . $theme . '/css_custom/global.css');
    } else {
        $css_file = file_get_contents(get_file_base() . '/themes/default/css/global.css');
    }
    $matches = array();
    if (preg_match('#\{\$THEME_WIZARD_COLOR,\#([a-f0-9][a-f0-9])([a-f0-9][a-f0-9])([a-f0-9][a-f0-9]),box_title_background,#i', $css_file, $matches) != 0) {
        $text_colour = imagecolorallocate($canvas, hexdec($matches[1]), hexdec($matches[2]), hexdec($matches[3]));
    } else {
        $text_colour = $white;
    }

    // Write text onto the canvas
    $do = array();
    if (($font_width > intval($logowizard_details['site_name_split'])) && (strpos($name, ' ') !== false)) { // Split in two
        if (has_ttf()) {
            list(, , $font_width, , , , , $font_height) = imagettfbbox(floatval($logowizard_details['site_name_font_size_small']), 0.0, $font_path, foxy_utf8_to_nce($name));
            $font_height = max($font_height, -$font_height);
        }
        $bits = explode(' ', $name);
        $a = '';
        $b = '';
        foreach ($bits as $bit) {
            if (strlen($a) < intval(round(floatval(strlen($name)) / 2.0))) {
                $a .= $bit . ' ';
            } else {
                $b .= $bit . ' ';
            }
        }
        $do[] = array($a, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset_small']) + $font_height, intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour);
        $do[] = array($b, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset_small']) + $font_height * 2 + intval($logowizard_details['site_name_split_gap']), intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour);
    } elseif ($font_width > intval($logowizard_details['site_name_split'])) { // Smaller font
        if (has_ttf()) {
            list(, , $font_width, , , , , $font_height) = imagettfbbox(floatval($logowizard_details['site_name_font_size_small']), 0.0, $font_path, foxy_utf8_to_nce($name));
            $font_height = max($font_height, -$font_height);
        }
        $do[] = array($name, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset']) + $font_height, intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour);
    } else { // Show normally
        $do[] = array($name, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset']) + $font_height, floatval($logowizard_details['site_name_font_size']), $font_path, $text_colour);
    }
    foreach ($do as $i => $doing) {
        if (has_ttf()) {
            imagettftext($canvas, (float)($doing[3]), 0.0, $doing[1], $doing[2], $doing[5], $doing[4], foxy_utf8_to_nce($doing[0]));
        } else {
            // @ needed for bizarre reasons due to type juggling in PHP (brought up by ocProducts PHP only)
            @imagestring($canvas, ($doing[3] == intval($logowizard_details['site_name_font_size_small'])) ? intval($logowizard_details['site_name_font_size_nonttf']) : $font, $doing[1], $doing[2] - 11, $doing[0], $doing[5]);
        }
    }

    // Output direct?
    imagesavealpha($canvas, true);
    if ($raw) {
        header('Content-type: image/png');
        //header('Content-Disposition: attachment; filename="-logo.png"');

        if (cms_srv('REQUEST_METHOD') == 'HEAD') {
            return '';
        }

        imagepng($canvas);
        imagedestroy($canvas);

        exit();
    }

    return $canvas;
}

/**
 * Make a theme. Note that this will trigger the AFM.
 *
 * @param  string $theme_name Name of the theme.
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  string $seed Seed colour to use.
 * @param  boolean $use Whether to use the theme immediately.
 * @param  ?boolean $dark Whether it will be a dark theme (null: autodetect).
 * @param  boolean $inherit_css Whether to inherit the CSS, for easier theme upgrading.
 */
function make_theme($theme_name, $source_theme, $algorithm, $seed, $use, $dark = false, $inherit_css = false)
{
    $GLOBALS['NO_QUERY_LIMIT'] = true;

    load_themewizard_params_from_theme($source_theme, $algorithm == 'hsv');

    if (file_exists(get_custom_file_base() . '/themes/' . $theme_name)) {
        require_code('abstract_file_manager');
        force_have_afm_details();
        $extending_existing = true;
    } else {
        if ($source_theme == 'default') {
            actual_add_theme($theme_name);
        } else {
            require_code('themes3');
            actual_copy_theme($source_theme, $theme_name);
        }
        $extending_existing = false;
    }

    if (($seed != find_theme_seed($source_theme)) || ($dark != find_theme_dark($source_theme))) {
        list($colours, $landscape) = calculate_theme($seed, $source_theme, $algorithm, 'colours', $dark);

        // Make images
        global $THEME_WIZARD_IMAGES, $THEME_WIZARD_IMAGES_NO_WILD, $THEME_IMAGES_CACHE;
        if (function_exists('imagecolorallocatealpha')) {
            require_code('themes2');
            $full_img_set = array();
            foreach ($THEME_WIZARD_IMAGES as $expression) {
                if (substr($expression, -1) == '*') {
                    $expression = substr($expression, 0, strlen($expression) - 2); // remove "/*"
                    $full_img_set = array_merge($full_img_set, array_keys(get_all_image_codes(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/images', $expression)));
                    $full_img_set = array_merge($full_img_set, array_keys(get_all_image_codes(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/images/' . fallback_lang(), $expression)));
                } else {
                    $full_img_set[] = $expression;
                }
            }

            if ($extending_existing) {
                $temp_all_ids = collapse_2d_complexity('id', 'path', $GLOBALS['SITE_DB']->query_select('theme_images', array('id', 'path'), array('theme' => $theme_name)));
            } else {
                $temp_all_ids = array();
            }

            $_langs = find_all_langs(true);

            foreach ($full_img_set as $image_code) {
                if (!in_array($image_code, $THEME_WIZARD_IMAGES_NO_WILD)) {
                    if (($extending_existing) && (array_key_exists($image_code, $temp_all_ids)) && (strpos($temp_all_ids[$image_code], $theme_name . '/images_custom/') !== false) && ((!url_is_local($temp_all_ids[$image_code])) || (file_exists(get_custom_file_base() . '/' . $temp_all_ids[$image_code])))) {
                        continue;
                    }

                    foreach (array_keys($_langs) as $lang) {
                        $orig_path = find_theme_image($image_code, true, true, $source_theme, $lang);
                        if ($orig_path == '') {
                            continue; // Theme has specified non-existent image as themewizard-compatible
                        }

                        if ((strpos($orig_path, '/' . $lang . '/') === false) && ($lang != fallback_lang())) {
                            continue;
                        }

                        if (strpos($orig_path, '/' . fallback_lang() . '/') !== false) {
                            $composite = 'themes/' . filter_naughty($theme_name) . '/images/' . $lang . '/';
                        } else {
                            $composite = 'themes/' . filter_naughty($theme_name) . '/images/';
                        }
                        $saveat = get_custom_file_base() . '/' . $composite . $image_code . '.png';
                        $saveat_url = $composite . $image_code . '.png';

                        // Wipe out ones that might have been copied from source theme
                        if (($source_theme != 'default') && (strpos($orig_path, 'images_custom') !== false)) {
                            foreach (array('png', 'jpg', 'gif', 'jpeg') as $ext) {
                                $old_delete_path = str_replace('/images/', '/images_custom/', basename($saveat, '.png')) . '.' . $ext;
                                @unlink($old_delete_path);
                                sync_file($old_delete_path);
                            }
                        }

                        if ((!file_exists($saveat)) || ($source_theme != 'default') || ($algorithm == 'hsv')) {
                            $image = calculate_theme($seed, $source_theme, $algorithm, $image_code, $dark, $colours, $landscape, $lang);
                            if (!is_null($image)) {
                                $pos = strrpos($image_code, '/');
                                if (($pos !== false) || (strpos($orig_path, '/' . fallback_lang() . '/') !== false)) {
                                    afm_make_directory($composite . substr($image_code, 0, $pos), true, true);
                                }
                                cms_imagesave($image, $saveat) or intelligent_write_error($saveat);
                                imagedestroy($image);
                                actual_edit_theme_image($image_code, $theme_name, $lang, $image_code, $saveat_url, true);
                            }
                        } else { // Still need to do the edit, as currently it'll have been mapped to the default theme when this theme was added
                            actual_edit_theme_image($image_code, $theme_name, $lang, $image_code, $saveat_url, true);
                        }
                    }
                }
            }
        }

        // Make sheets
        $dh = opendir(get_file_base() . '/themes/' . filter_naughty($source_theme) . (($source_theme == 'default') ? '/css/' : '/css_custom/'));
        while (($sheet = readdir($dh)) !== false) {
            if (substr($sheet, -4) == '.css') {
                $saveat = get_custom_file_base() . '/themes/' . filter_naughty($theme_name) . '/css_custom/' . $sheet;
                if ((!file_exists($saveat)) || ($source_theme != 'default') || ($algorithm == 'hsv')) {
                    if ($inherit_css) {
                        $output = '{+START,CSS_INHERIT,' . basename($sheet, '.css') . ',' . filter_naughty($source_theme) . ',' . $seed . ',' . ($dark ? '1' : '0') . ',' . $algorithm . '}{+END}';
                    } else {
                        $output = theme_wizard_colours_to_sheet($sheet, $landscape, $source_theme, $algorithm, $seed);
                    }
                    $default_version_path = get_file_base() . '/themes/default/css/' . $sheet;
                    if (is_file($default_version_path)) {
                        $default_version = file_get_contents($default_version_path);
                        $changed_from_default_theme = file_get_contents(unixify_line_format($default_version_path)) != $output;
                    } else {
                        $changed_from_default_theme = true;
                    }
                    if ($changed_from_default_theme) {
                        require_code('files');
                        cms_file_put_contents_safe(get_custom_file_base() . '/themes/' . filter_naughty($theme_name) . '/css_custom/' . $sheet, $output, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE);
                        if (!$inherit_css) {
                            $c_success = @copy(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/css/' . $sheet, $saveat . '.editfrom');
                            if ($c_success !== false) {
                                fix_permissions($saveat . '.editfrom');
                                sync_file($saveat . '.editfrom');
                            }
                        } else {
                            @unlink($saveat . '.editfrom');
                            sync_file($saveat . '.editfrom');
                        }
                    }
                }
            }
        }
    }

    // Use it, if requested
    if ($use) {
        $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'zones SET zone_theme=\'' . db_escape_string($theme_name) . '\' WHERE ' . db_string_not_equal_to('zone_name', 'cms') . ' AND ' . db_string_not_equal_to('zone_name', 'adminzone'));

        require_code('permissions2');
        set_global_category_access('theme', $theme_name);

        erase_persistent_cache();
    }
}

/**
 * Output a theme component straight to the browser.
 */
function themewizard_script()
{
    $type = get_param_string('type');
    $source_theme = get_param_string('keep_theme_source', 'default');
    $algorithm = get_param_string('keep_theme_algorithm', 'equations');
    $show = get_param_string('show');
    $seed = get_param_string('keep_theme_seed');
    if ($seed == 'kiddie') {
        $seed = str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT);
    }
    $_dark = get_param_integer('keep_theme_dark', null);
    $dark = is_null($_dark) ? null : ($_dark == 1);
    if ($type == 'preview') {
        $_tpl = do_template('THEMEWIZARD_2_PREVIEW');
        $tpl = do_template('STANDALONE_HTML_WRAP', array('_GUID' => '652b7df378b36714cb9dfa146490cbb8', 'TITLE' => do_lang_tempcode('PREVIEW'), 'CONTENT' => $_tpl, 'FRAME' => true));
        $tpl->evaluate_echo();
    }
    if ($type == 'css' || $type == 'css_raw') {
        safe_ini_set('ocproducts.xss_detect', '0');
        require_code('tempcode_compiler');
        list($colours, $landscape) = calculate_theme($seed, $source_theme, $algorithm, 'colours', $dark);
        if ($show != 'global.css') { // We need to make sure the global.css file is parsed, as it contains some shared THEME_WIZARD_COLOR variables that Tempcode will pick up on
            $css = theme_wizard_colours_to_sheet('global.css', $landscape, $source_theme, $algorithm, $seed);
            $tpl = template_to_tempcode($css);
            $tpl->evaluate();
        }
        $css = theme_wizard_colours_to_sheet($show, $landscape, $source_theme, $algorithm, $seed);
        header('Content-type: text/css');
        if ($type == 'css') {
            $tpl = template_to_tempcode($css);
            $tpl->evaluate_echo();
        } else {
            echo $css;
        }
    }
    if ($type == 'image') {
        $image = calculate_theme($seed, $source_theme, $algorithm, $show, $dark);
        if (is_null($image)) {
            header('Location: ' . escape_header(find_theme_image($show)));
            exit();
        }

        $saveat = cms_tempnam();
        cms_imagesave($image, $saveat, 'png') or intelligent_write_error($saveat);

        imagedestroy($image);

        cms_ob_end_clean();

        header('Content-type: image/png');

        readfile($saveat);

        @unlink($saveat);
    }
}

/**
 * Calculate some component relating to a theme from a colour seed.
 *
 * @param  string $seed Colour seed.
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $show What to generate ('colours', or the name of a theme image).
 * @param  ?boolean $dark Whether it will be a dark theme (null: autodetect).
 * @param  ?array $colours The colour map to use (null: compute).
 * @param  ?array $landscape The computed colour landscape to use (null: compute).
 * @param  ?LANGUAGE_NAME $lang The language to work in (null: default).
 * @return mixed Image resource OR A pair: extended map of colours, colour expression landscape
 */
function calculate_theme($seed, $source_theme, $algorithm, $show = 'colours', $dark = null, $colours = null, $landscape = null, $lang = null)
{
    if ($seed[0] == '#') {
        $seed = substr($seed, 1);
    }

    $white = 255;
    $black = 0;

    // Strip and decimalize the three colors
    $red = hexdec(substr($seed, 0, 2));
    $green = hexdec(substr($seed, 2, 2));
    $blue = hexdec(substr($seed, 4, 2));

    // Decide the dominant color

    if (($red == $blue) && ($red != $green)) {
        $dominant = 'purple';
    }
    if (($red == $green) && ($red != $blue)) {
        $dominant = 'yellow';
    }
    if (($blue == $green) && ($blue != $red)) {
        $dominant = 'cyan';
    }

    if (($red == $green) && ($red == $blue)) {
        if ($red >= 185) {
            $dominant = 'white';
        }
        if (($red < 185) && ($red >= 100)) {
            $dominant = 'gray';
        }
        if ($red < 100) {
            $dominant = 'black';
        }
    }

    if (($red > $green) && ($red > $blue)) {
        $dominant = 'red';
    }
    if (($green > $red) && ($green > $blue)) {
        $dominant = 'green';
    }
    if (($blue > $red) && ($blue > $green)) {
        $dominant = 'blue';
    }

    // Decide if this is a "light" skin or a "dark" theme
    if (((intval(round(floatval($red + $green + $blue) / 3.0)) >= 127) || ($dark === false)) && ($dark !== true)) {
        $light_dark = 'light';
        $anti_light_dark = 'dark';
        $wb = 'FFFFFF';
        $awb = '000000';
    } else {
        $light_dark = 'dark';
        $anti_light_dark = 'light';
        $wb = '000000';
        $awb = 'FFFFFF';
    }

    if ((is_null($landscape)) || (is_null($colours))) {
        $colours = array(
            // Hints for computation
            'dark' => ($light_dark == 'dark') ? '1' : '0',
            'red' => strval($red),
            'green' => strval($green),
            'blue' => strval($blue),
            'dominant' => $dominant,
            'LD' => $light_dark,
            'DL' => $anti_light_dark,

            // Actual colours
            'seed' => $seed,
            'WB' => $wb,
            'BW' => $awb,
        );
        if ($algorithm == 'equations') {
            list($colours, $landscape) = calculate_dynamic_css_colours($colours, $source_theme);
        } else {
            $landscape = array();
        }
    }

    if ($show != 'colours') {
        unset($_GET['keep_theme_seed']);
        $ti = find_theme_image($show, false, true, $source_theme, $lang);
        if ($ti == '') {
            return null;
        }
        $path = get_file_base() . '/' . $ti;
        if (!file_exists($path)) { // File since deleted, we'll revert
            $ti = find_theme_image($show, false, true, $source_theme, $lang, null, true);
            if ($ti == '') {
                return null;
            }
            $path = get_file_base() . '/' . $ti;
        }

        $img = null;
        if (function_exists('imagecolorallocatealpha')) {
            if ($algorithm == 'hsv') {
                $img = re_hue_image($path, $seed, $source_theme, true);
            } else {
                if ($source_theme == 'default') {
                    $needed = array('washed_out', 'area_background', 'lgrad', 'dgrad', 'dark_border', 'comcode_quote_left', 'comcode_quote_right', 'a.link', 'a.hover', 'a.link__dark', 'a.hover__dark', 'special_borderer', 'cnsredirectindicator', 'cnspostindicator', 'slightly_seeded_text', 'special_middle',);
                    foreach ($needed as $colour_needed) {
                        if (!array_key_exists($colour_needed, $colours)) {
                            warn_exit(do_lang_tempcode('UNRESOLVABLE_COLOURS', escape_html($colour_needed)));
                        }
                    }

                    if ($show == 'gradient') {
                        $img = generate_gradient($colours['lgrad'], $colours['dgrad']);
                    } elseif (($show == 'icons/16x16/filetypes/external_link')) {
                        $img = generate_recoloured_image($path, '#000000', $colours['BW'], '#000000', $colours['BW']);
                    } elseif (($show == 'background_image')) {
                        $img = generate_recoloured_image($path, '#FFFFFF', $colours['WB'], '#DDE5F7', $colours['washed_out']);
                    } elseif (($show == 'header') || ($show == 'outer_background') || ($show == 'inner_background') || ($show == 'block_background') || ($show == 'big_tabs_controller_button_active') || ($show == 'big_tabs_controller_button_top_active') || ($show == 'big_tabs_controller_button_top') || ($show == 'big_tabs_controller_button')) {
                        $img = re_hue_image($path, $seed, $source_theme, false, $light_dark == 'dark');
                    } elseif ($show == 'quote_gradient') {
                        $img = generate_recoloured_image($path, '#072A66', $colours['dark_border'], '#C7D5EC', $colours['comcode_quote_left'], '#8CA7D2', $colours['comcode_quote_right'], 'horizontal');
                    } elseif ($show == 'menu_bullet') {
                        $img = generate_recoloured_image($path, '#000000', $colours['a.link'], '#190406', $colours['a.link']);
                    } elseif ($show == 'menu_bullet_current') {
                        $img = generate_recoloured_image($path, '#00A55A', $colours['a.hover'], '#00A55A', $colours['a.hover']);
                    } elseif ($show == 'menu_bullet_hover') {
                        $img = generate_recoloured_image($path, '#9C202F', $colours['a.hover'], '#BA1621', $colours['a.hover']);
                    } elseif ($show == 'tab') {
                        $img = generate_recoloured_image($path, '#B5B5B5', $colours['tab_border'], '#F4F4F4', $colours['area_5_background']);
                    } elseif (substr($show, 0, 10) == 'checklist/') {
                        $img = generate_recoloured_image($path, '#335082', $colours['special_borderer'], '#091C3D', $colours['special_middle']);
                    } elseif ($show == '1x/arrow_box' || $show == '2x/arrow_box') {
                        $img = generate_recoloured_image($path, '#12467A', $colours['a.link'], '#0A223D', $colours['a.link__dark']);
                    } elseif ($show == '1x/arrow_box_hover' || $show == '2x/arrow_box_hover') {
                        $img = generate_recoloured_image($path, '#12467A', $colours['a.hover'], '#0A223D', $colours['a.hover__dark']);
                    } elseif (in_array($show, array('cns_general/no_new_posts_redirect', 'cns_general/new_posts_redirect'))) {
                        $img = generate_recoloured_image($path, '#FFFFFF', '#FFFFFF', '#549B8C', $colours['cnsredirectindicator']);
                    } elseif (in_array($show, array('cns_general/redirect', 'cns_general/redirect', 'cns_general/no_new_posts', 'cns_general/new_posts'))) {
                        $img = generate_recoloured_image($path, '#FFFFFF', '#FFFFFF', '#5A84C4', $colours['cnspostindicator']);
                    } else { // These are less special... we just change the hue
                        $img = re_hue_image($path, $seed, $source_theme);
                    }
                } else {
                    $img = re_hue_image($path, $seed, $source_theme);
                }
            }
        }

        return $img;
    }

    return array($colours, $landscape);
}

/**
 * Augment an array of CSS colours with colours that are derived actually inside the CSS-sheets.
 *
 * @param  array $colours Map of colours.
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @return array A pair: extended map of colours, colour expression landscape
 */
function calculate_dynamic_css_colours($colours, $source_theme)
{
    $theme = filter_naughty($source_theme);
    $css_dir = (($theme == 'default') ? 'css' : 'css_custom');
    $dh = opendir(get_file_base() . '/themes/' . $theme . '/' . $css_dir . '/');

    require_lang('themes');

    // Initialise landscape
    $landscape = array();
    foreach ($colours as $key => $val) {
        if (preg_match('#^[0-9a-f]{6}$#i', $val) != 0) {
            $landscape[$key] = array(
                $key, // Colour name
                null, // Parsed expression
                null, // Full match string
                $val // Final colour
            );
        }
    }

    // First we build up our landscape
    while (($sheet = readdir($dh)) !== false) {
        if (substr($sheet, -4) == '.css') {
            $path = get_file_base() . '/themes/' . $theme . '/' . $css_dir . '/' . $sheet;
            $contents = unixify_line_format(file_get_contents($path));

            $matches = array();
            $num_matches = preg_match_all('#\{\$THEME_WIZARD_COLOR,(.*),(.*),(.*)\}#', $contents, $matches);

            for ($i = 0; $i < $num_matches; $i++) {
                // Skip over our little stored hints (not intended for calculation, comes with new seed)
                if (in_array($matches[2][$i], array('seed', 'WB', 'BW'))) {
                    continue;
                }

                // A one we're really interested in
                $parsed = parse_css_colour_expression($matches[3][$i]);
                if (!is_null($parsed)) {
                    $landscape[] = array(
                        $matches[2][$i], // Colour name
                        $parsed, // Parsed expression
                        $matches[0][$i], // Full match string
                        null // Final colour
                    );
                }
            }
        }
    }

    // Then we resolve our expressions
    $resolved_landscaped = array();
    $safety_count = 0;
    while (count($landscape) != 0) {
        foreach ($landscape as $i => $peak) {
            if (is_null($peak[3])) {
                $peak[3] = execute_css_colour_expression($peak[1], $colours);
            }
            if (!is_null($peak[3])) { // We were able to get a result
                $resolved_landscaped[] = $peak;
                unset($landscape[$i]);

                // Then we add to the colours array
                if ($peak[0] != 'wizard') { // 'wizard' is a generic name, so we ignore it
                    $colours[$peak[0]] = $peak[3];
                }
            }
        }
        $safety_count++;
        if ($safety_count == 100) {
            $_landscape = '';
            foreach ($landscape as $x) {
                if ($_landscape != '') {
                    $_landscape .= '; ';
                }
                $_landscape .= $x[2];
            }
            warn_exit(do_lang_tempcode('UNRESOLVABLE_COLOURS', escape_html($_landscape)));
        }
    }

    return array($colours, $resolved_landscaped);
}

/**
 * Convert a textual CSS colour expression into an expression tree.
 *
 * @param  string $textual Textual expression.
 * @return ?array Expression tree (null: not real).
 */
function parse_css_colour_expression($textual)
{
    // '*' is inserted after a %, and then % is dropped
    $textual = preg_replace('#(^| )(\d+)%#', '\\1\\2 *', $textual);

    // We're using spaces as token delimiters, so we need to do a trim to clean up, and also put spaces around brackets
    $textual = trim(str_replace(')', ' )', str_replace('(', '( ', $textual)));

    // Perform inner conversion
    $tokens = explode(' ', $textual);

    $expression = _parse_css_colour_expression($tokens);
    return $expression;
}

/**
 * Convert CSS colour tokens into an expression tree.
 *
 * @param  array $tokens Tokens.
 * @return ?array Expression tree (null: error).
 *
 * @ignore
 */
function _parse_css_colour_expression($tokens)
{
    // We now scan through, structuring into an evaluation-order tree (but not an expression tree  at the level we're operating on)
    // Brackets
    $new_tokens = array();
    for ($i = 0; $i < count($tokens); $i++) {
        if ($tokens[$i] === '(') {
            // Find matching closing token
            $extra_opened = 0;
            $sub_tokens = array();
            for ($i = $i + 1; $i < count($tokens); $i++) {
                if ($tokens[$i] == '(') {
                    $extra_opened++;
                } elseif (($tokens[$i] == ')') && ($extra_opened > 0)) {
                    $extra_opened--;
                } elseif ($tokens[$i] == ')') {
                    $new_tokens[] = _parse_css_colour_expression($sub_tokens);
                    break;
                }
                $sub_tokens[] = $tokens[$i];
            }
        } else {
            $new_tokens[] = $tokens[$i];
        }
    }
    $tokens = $new_tokens;
    // Additions. Each addition is a pivot.
    for ($i = 0; $i < count($tokens); $i++) {
        if ($tokens[$i] === '+') {
            return array('+', _parse_css_colour_expression(array_slice($tokens, 0, $i)), _parse_css_colour_expression(array_slice($tokens, $i + 1)));
        }
    }

    // Either we have a single token
    if (count($tokens) == 1) {
        return $tokens[0];
    }

    // Or we have a length of more than 3 tokens, in which case we pivot
    if (count($tokens) > 3) {
        return array($tokens[1], $tokens[0], _parse_css_colour_expression(array_slice($tokens, 2)));
    }

    // Or we have just 3 tokens, a single operation
    if (!array_key_exists(2, $tokens)) {
        return null;
    }
    return array($tokens[1], $tokens[0], $tokens[2]);
}

/**
 * Execute CSS colour expression.
 *
 * @param  mixed $expression Expression tree (array) OR leaf (string).
 * @param  array $colours Known colours at this point.
 * @return ?string RRGGBB colour or possibly just a number (null: answer cannot be computed).
 */
function execute_css_colour_expression($expression, $colours)
{
    if (!is_array($expression)) {
        if (preg_match('#^[0-9A-Fa-f]{6}$#', $expression) != 0) {
            return $expression;
        }
        if (preg_match('#^\#[0-9A-Fa-f]{6}$#', $expression) != 0) {
            return substr($expression, 1);
        }
        if (preg_match('#^[\+\-]?\d+$#', $expression) != 0) {
            return $expression;
        }

        foreach ($colours as $colour => $actual_colour) {
            if ($colour == $expression) {
                return $actual_colour;
            }
        }

        return null; // Couldn't find it - we'll have to instruct it to come back to it
    }

    $operation = $expression[0];
    $operand_a = execute_css_colour_expression($expression[1], $colours);
    if (is_null($operand_a)) {
        return null;
    }
    $operand_b = execute_css_colour_expression($expression[2], $colours);
    if (is_null($operand_b)) {
        return null;
    }

    if ($operation[0] == '&') {
        $operand_c = str_replace('%', '', substr($operation, 1));
        $operation = '&';
    }

    switch ($operation) {
        /* These are percentage modifiers */

        case 'sat_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = ($s == 0) ? hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour(255 * intval($operand_b) / (100.0)))) : hsv_to_rgb(floatval($h), floatval(fix_colour(255 * intval($operand_b) / (100.0))), floatval($v));
            break;

        case 'sat_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval(fix_colour($s + intval($operand_b))), floatval($v));
            break;

        case 'sat':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval(fix_colour($s * intval($operand_b) / (100.0))), floatval($v));
            break;

        case 'val_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour(255 * intval($operand_b) / (100.0))));
            break;

        case 'val_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour($v + intval($operand_b))));
            break;

        case 'val':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour($v * intval($operand_b) / (100.0))));
            break;

        case 'hue_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour(255 * intval($operand_b) / (100.0), true)), floatval($s), floatval($v));
            break;

        case 'hue_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour($h + intval($operand_b), true)), floatval($s), floatval($v));
            break;

        case 'hue':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour($h * intval($operand_b) / (100.0), true) % 255), floatval($s), floatval($v));
            break;

        case '&':
            $red = hexdec(substr($operand_a, 0, 2));
            $green = hexdec(substr($operand_a, 2, 2));
            $blue = hexdec(substr($operand_a, 4, 2));
            $fraction = 1.0 - $operand_c / 100.0;

            $red_b = hexdec(substr($operand_b, 0, 2));
            $green_b = hexdec(substr($operand_b, 2, 2));
            $blue_b = hexdec(substr($operand_b, 4, 2));

            $red = intval($fraction * $red + (1 - $fraction) * $red_b);
            $green = intval($fraction * $green + (1 - $fraction) * $green_b);
            $blue = intval($fraction * $blue + (1 - $fraction) * $blue_b);

            $result = str_pad(dechex(fix_colour($red)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(fix_colour($green)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(fix_colour($blue)), 2, '0', STR_PAD_LEFT);
            break;

        case '*':
            $red = intval(round(floatval(hexdec(substr($operand_b, 0, 2)) * intval($operand_a)) / (100.0)));
            $green = intval(round(floatval(hexdec(substr($operand_b, 2, 2)) * intval($operand_a)) / (100.0)));
            $blue = intval(round(floatval(hexdec(substr($operand_b, 4, 2)) * intval($operand_a)) / (100.0)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        /* These are communicative combinators */

        case '+':
            $red = fix_colour(hexdec(substr($operand_a, 0, 2)) + hexdec(substr($operand_b, 0, 2)));
            $green = fix_colour(hexdec(substr($operand_a, 2, 2)) + hexdec(substr($operand_b, 2, 2)));
            $blue = fix_colour(hexdec(substr($operand_a, 4, 2)) + hexdec(substr($operand_b, 4, 2)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        case '-':
            $red = fix_colour(hexdec(substr($operand_a, 0, 2)) - hexdec(substr($operand_b, 0, 2)));
            $green = fix_colour(hexdec(substr($operand_a, 2, 2)) - hexdec(substr($operand_b, 2, 2)));
            $blue = fix_colour(hexdec(substr($operand_a, 4, 2)) - hexdec(substr($operand_b, 4, 2)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        /* These are miscellaneous */

        case 'shift':
            if (intval($operand_b) == 1) {
                $result = substr($operand_a, 4) . substr($operand_a, 0, 4);
            } else {
                $result = substr($operand_a, 2) . substr($operand_a, 0, 2);
            }
            break;
    }

    return $result;
}

/**
 * Make sure a colour component fits within the necessary range (0<=x<256).
 *
 * @param  mixed $x Colour component (float or integer).
 * @param  boolean $hue Whether this is hue (meaning it cycles around)
 * @return integer Constrained colour component.
 */
function fix_colour($x, $hue = false)
{
    if (is_float($x)) {
        $x = intval(round($x));
    }

    if ($hue) {
        while ($x > 255) {
            $x -= 255;
        }
        while ($x < 0) {
            $x += 255;
        }
    } else {
        if ($x > 255) {
            $x = 255;
        }
        if ($x < 0) {
            $x = 0;
        }
    }

    return $x;
}

/**
 * Convert an RGB colour to HSV colour components. Based on publicly distributed code fragments which were themselves based on others: reasonably assumed as public domain.
 *
 * @param  string $rgb RRGGBB colour.
 * @return array Triplet of (0-255) components: H, S, V
 */
function rgb_to_hsv($rgb)
{
    $red = hexdec(substr($rgb, 0, 2));
    $green = hexdec(substr($rgb, 2, 2));
    $blue = hexdec(substr($rgb, 4, 2));

    $r = $red / 255.0;
    $g = $green / 255.0;
    $b = $blue / 255.0;
    $h = 0.0;
    $s = 0.0;
    $v = 0.0;
    $min = min($r, $g, $b);
    $max = max($r, $g, $b);
    $delta = $max - $min;

    $v = $max;

    if ($delta == 0.0) {
        $h = 0.0;
        $s = 0.0;
    } else {
        $s = $delta / $max;

        $d_r = ((($max - $r) / 6) + ($delta / 2)) / $delta;
        $d_g = ((($max - $g) / 6) + ($delta / 2)) / $delta;
        $d_b = ((($max - $b) / 6) + ($delta / 2)) / $delta;

        if ($r == $max) {
            $h = $d_b - $d_g;
        } elseif ($g == $max) {
            $h = (1.0 / 3.0) + $d_r - $d_b;
        } else {
            $h = (2.0 / 3.0) + $d_g - $d_r;
        }

        if ($h < 0.0) {
            $h++;
        } elseif ($h > 1.0) {
            $h--;
        }
    }

    return array(intval(round($h * 255)), intval(round($s * 255)), intval(round($v * 255)));
}

/**
 * Convert HSV colour components to an RGB colour. Based on publicly distributed code fragments which were themselves based on others: reasonably assumed as public domain.
 *
 * @param  float $h H component
 * @param  float $s S component
 * @param  float $v V component
 * @return string RGB colour.
 */
function hsv_to_rgb($h, $s, $v)
{
    $h = 6.0 * $h / 255.0;
    $s = $s / 255.0;
    $v = $v / 255.0;

    if ($s == 0.0) {
        $r = $v;
        $g = $v;
        $b = $v;
    } else {
        $hi = intval(floor($h));
        $f = $h - $hi;
        $p = ($v * (1.0 - $s));
        $q = ($v * (1.0 - ($f * $s)));
        $t = ($v * (1.0 - ((1.0 - $f) * $s)));

        switch ($hi) {
            case 0:
                $r = $v;
                $g = $t;
                $b = $p;
                break;
            case 1:
                $r = $q;
                $g = $v;
                $b = $p;
                break;
            case 2:
                $r = $p;
                $g = $v;
                $b = $t;
                break;
            case 3:
                $r = $p;
                $g = $q;
                $b = $v;
                break;
            case 4:
                $r = $t;
                $g = $p;
                $b = $v;
                break;
            default:
                $r = $v;
                $g = $p;
                $b = $q;
                break;
        }
    }

    return str_pad(dechex(fix_colour(intval(round($r * 255)))), 2, '0', STR_PAD_LEFT) .
           str_pad(dechex(fix_colour(intval(round($g * 255)))), 2, '0', STR_PAD_LEFT) .
           str_pad(dechex(fix_colour(intval(round($b * 255)))), 2, '0', STR_PAD_LEFT);
}

/**
 * Rewrite some CSS code according to a CSS landscape.
 *
 * @param  ID_TEXT $sheet CSS filename of source file.
 * @param  array $landscape The colour expression landscape which we'll make substitutions using.
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $seed The seed colour
 * @return string The sheet
 */
function theme_wizard_colours_to_sheet($sheet, $landscape, $source_theme, $algorithm, $seed)
{
    $theme = filter_naughty($source_theme);

    $path = get_file_base() . '/themes/' . $theme . '/css_custom/' . filter_naughty($sheet);
    if (!file_exists($path)) {
        $path = get_file_base() . '/themes/' . $theme . '/css/' . filter_naughty($sheet);
    }
    if (!file_exists($path)) {
        $path = get_file_base() . '/themes/default/css_custom/' . filter_naughty($sheet);
    }
    if (!file_exists($path)) {
        $path = get_file_base() . '/themes/default/css/' . filter_naughty($sheet);
    }
    if (!file_exists($path)) {
        return ''; // Probably a dynamic theme wizard call after an addon was removed
    }

    $contents = unixify_line_format(file_get_contents($path));

    return theme_wizard_colours_to_css($contents, $landscape, $source_theme, $algorithm, $seed);
}

/**
 * Rewrite some CSS code according to a CSS landscape.
 *
 * @param  string $contents CSS to apply to.
 * @param  array $landscape The colour expression landscape which we'll make substitutions using.
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $seed The seed colour
 * @return string The sheet
 */
function theme_wizard_colours_to_css($contents, $landscape, $source_theme, $algorithm, $seed)
{
    if ($algorithm == 'hsv') {
        list($composr_h, $composr_s, $composr_v) = rgb_to_hsv(find_theme_seed($source_theme, true));
        list($desired_h, $desired_s, $desired_v) = rgb_to_hsv($seed);
        $hue_dif = $desired_h - $composr_h;
        $sat_dif = 0;//$desired_s-$composr_s;     Actually causes weirdness
        $val_dif = $desired_v - $composr_v;

        $matches = array();
        $num_matches = preg_match_all('#\#([A-Fa-f0-9]{3,6})([^A-Fa-f0-9])#', $contents, $matches);
        for ($i = 0; $i < $num_matches; $i++) {
            list($h, $s, $v) = rgb_to_hsv((strlen($matches[1][$i]) == 3) ? ($matches[1][$i][0] . $matches[1][$i][0] . $matches[1][$i][1] . $matches[1][$i][1] . $matches[1][$i][2] . $matches[1][$i][2]) : $matches[1][$i]);
            $new_colour = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval(fix_colour($s + $sat_dif)), floatval(fix_colour($v + $val_dif)));
            $contents = str_replace(array(strtolower($matches[0][$i]), strtoupper($matches[0][$i])), array('#' . $new_colour . $matches[2][$i], '#' . $new_colour . $matches[2][$i]), $contents);
        }

        return $contents;
    }
    foreach ($landscape as $peak) {
        if (!is_null($peak[2])) {
            $from = $peak[2];
            $to = preg_replace('#\{\$THEME_WIZARD_COLOR,\#[\da-fA-F]{6},#', '{$THEME_WIZARD_COLOR,#' . $peak[3] . ',', $peak[2]);
            $contents = str_ireplace($from, $to, $contents);
        } else {
            $to = '{$THEME_WIZARD_COLOR,#' . $peak[3] . ',' . $peak[0] . ',100% ' . $peak[3] . '}';
            $contents = preg_replace('#\{\$THEME_WIZARD_COLOR,\#[\da-fA-F]{6},' . $peak[0] . ',100% [\da-fA-F]{6}\}#i', $to, $contents);
        }
    }

    // Some hints not calculated by equations need separate replacements
    $contents = str_replace('/* Used to initiate equations (although running the Theme Wizard replaces these with what the user chooses - which is how it works) */' . "\n", '', $contents);
    $contents = str_ireplace('/*Theme seed is: ' . find_theme_seed('default') . '*/', '/*Theme seed is: ' . $seed . '*/', $contents);

    return $contents;
}

/**
 * Generate a theme image by converting an existing one to a new colour scheme via re-hueing.
 *
 * @param  mixed $path The image path OR a preloaded GD image resource
 * @param  string $seed The colour code of our hue
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  boolean $also_s_and_v Whether to also adjust the S and V components
 * @param  boolean $invert Whether to invert the colours
 * @return resource The image
 */
function re_hue_image($path, $seed, $source_theme, $also_s_and_v = false, $invert = false)
{
    list($composr_h, $composr_s, $composr_v) = rgb_to_hsv(find_theme_seed($source_theme));
    list($seed_h, $seed_s, $seed_v) = rgb_to_hsv($seed);
    $hue_dif = $seed_h - $composr_h;
    $sat_dif = $seed_s - $composr_s;
    $val_dif = $seed_v - $composr_v;

    if (is_string($path)) {
        $_image = cms_imagecreatefrom($path);
        if ($_image === false) {
            warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($path)));
        }
    } else {
        $_image = $path;
    }

    $width = imagesx($_image);
    $height = imagesy($_image);
    if (function_exists('imageistruecolor')) {
        if (function_exists('imagecreatetruecolor')) {
            $trans_colour = imagecolortransparent($_image); // Even a truecolor one can have a transparency currency, if 24 bit

            if (!imageistruecolor($_image)) {
                $image = imagecreatetruecolor($width, $height);
                imagealphablending($image, false);

                $transparent = imagecolortransparent($_image);
                if ($transparent >= imagecolorstotal($_image)) { // Workaround for corrupt images
                    $transparent = -1;
                }
                if ($transparent != -1) {
                    $_transparent = imagecolorsforindex($_image, $transparent);
                    imagecolortransparent($image, imagecolorallocate($image, $_transparent['red'], $_transparent['green'], $_transparent['blue']));
                }

                imagecopy($image, $_image, 0, 0, 0, 0, $width, $height);
            } else {
                $image = $_image;
            }
        } else {
            $image = $_image;
            $trans_colour = imagecolortransparent($_image);
        }
    } else {
        $image = $_image;
        $trans_colour = imagecolortransparent($_image);
    }
    if ($trans_colour === -1 || $trans_colour === false) {
        $trans_colour = null;
    }
    imagealphablending($image, false);
    imagesavealpha($image, true);

    for ($y = 0; $y < $height; $y++) {
        for ($x = 0; $x < $width; $x++) {
            $_existing_colour = imagecolorat($image, $x, $y);
            $existing_colour = imagecolorsforindex($image, $_existing_colour);
            if (!is_null($trans_colour)) {
                $__existing_colour = imagecolorat($_image, $x, $y);
                if ($__existing_colour == $trans_colour) {
                    $existing_colour['alpha'] = 127;
                }
            }

            $r = $existing_colour['red'];
            $g = $existing_colour['green'];
            $b = $existing_colour['blue'];
            $a = $existing_colour['alpha'];

            list($h, $s, $v) = rgb_to_hsv(str_pad(dechex($r), 2, '0', STR_PAD_LEFT) . str_pad(dechex($g), 2, '0', STR_PAD_LEFT) . str_pad(dechex($b), 2, '0', STR_PAD_LEFT));
            if ($invert) {
                $v = 255 - $v;
                $v = min($v * 3, 255); // Because it's harder to see deviations of black
            }
            if ($seed_s < 10) {
                $s = $seed_s; // To stop red colours for gray-scale images
            }
            if ($also_s_and_v) {
                $sat_dif = 0; // Actually causes weirdness
                $result = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval(fix_colour($s + $sat_dif)), floatval(fix_colour($v + $val_dif)));
            } else {
                $result = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval($s), floatval($v));
            }

            $new_colour_r = hexdec(substr($result, 0, 2));
            $new_colour_g = hexdec(substr($result, 2, 2));
            $new_colour_b = hexdec(substr($result, 4, 2));

            if (function_exists('imagecolorallocatealpha')) {
                $target_colour = imagecolorallocatealpha($image, $new_colour_r, $new_colour_g, $new_colour_b, $a);
            } else {
                $target_colour = imagecolorallocate($image, $new_colour_r, $new_colour_g, $new_colour_b);
            }
            imagesetpixel($image, $x, $y, $target_colour);
        }
    }

    return $image;
}

/**
 * Generate a gradient for a theme.
 *
 * @param  string $top Colour for the top.
 * @param  string $bottom Colour for the bottom.
 * @return resource The image
 */
function generate_gradient($top, $bottom)
{
    $gradient = imagecreate(1, 27);
    $width = 27;

    $topred = intval(base_convert(substr($top, 0, 2), 16, 10));
    $topgrn = intval(base_convert(substr($top, 2, 2), 16, 10));
    $topblu = intval(base_convert(substr($top, 4, 2), 16, 10));

    $botred = intval(base_convert(substr($bottom, 0, 2), 16, 10));
    $botgrn = intval(base_convert(substr($bottom, 2, 2), 16, 10));
    $botblu = intval(base_convert(substr($bottom, 4, 2), 16, 10));

    $dr = ($botred - $topred) / $width;
    $dg = ($botgrn - $topgrn) / $width;
    $db = ($botblu - $topblu) / $width;

    for ($i = 0; $i < $width; $i++) {
        $color = imagecolorallocate($gradient, $topred + intval(round($dr * floatval($i))), $topgrn + intval(round($dg * floatval($i))), $topblu + intval(round($db * floatval($i))));
        imagesetpixel($gradient, 0, $i, $color);
    }

    return $gradient;
}

/**
 * Generate a theme image by converting an existing one to a new colour scheme via intelligent blending correlation.
 *
 * @param  mixed $path The image path OR a preloaded GD image resource
 * @param  string $colour_a_orig The colour code of what we have as our "minor" colour (often a border colour)
 * @param  string $colour_a_new The colour code of what we want as our "minor" colour (often a border colour)
 * @param  string $colour_b1_orig The colour code of what we have as our first major colour (often the only major colour)
 * @param  string $colour_b1_new The colour code of what we want as our first major colour (often the only major colour)
 * @param  ?string $colour_b2_orig The colour code of what we have as our second major colour (the gradient target, at the bottom/right of the image) (null: not gradiented)
 * @param  ?string $colour_b2_new The colour code of what we want as our second major colour (the gradient target, at the bottom/right of the image) (null: not gradiented)
 * @param  string $gradient_direction The directional code for the gradient
 * @set    vertical horizontal
 * @param  ?array $pixel_x_start_array An array that is used to limit where we do our conversion on. It specifies, for each y-offset, the x-offset we start from (null: no such limitation)
 * @param  integer $gradient_offset What the gradient assumed start-position will be offset by (in the gradient direction).
 * @param  boolean $end_array Whether the pixel_x_start array is actually an end array.
 * @return resource The image
 */
function generate_recoloured_image($path, $colour_a_orig, $colour_a_new, $colour_b1_orig, $colour_b1_new, $colour_b2_orig = null, $colour_b2_new = null, $gradient_direction = 'vertical', $pixel_x_start_array = null, $gradient_offset = 0, $end_array = false)
{
    /*$colour_a_new = $colour_a_orig;  For testing: a null conversion
    $colour_b1_new = $colour_b1_orig;
    $colour_b2_new = $colour_b2_orig;*/

    $colour_a_orig = str_replace('#', '', $colour_a_orig);
    $colour_b1_orig = str_replace('#', '', $colour_b1_orig);
    if (!is_null($colour_b2_new)) {
        $colour_b2_orig = str_replace('#', '', $colour_b2_orig);
    }
    $colour_a_new = str_replace('#', '', $colour_a_new);
    $colour_b1_new = str_replace('#', '', $colour_b1_new);
    if (!is_null($colour_b2_new)) {
        $colour_b2_new = str_replace('#', '', $colour_b2_new);
    }
    $colour_a_orig_r = hexdec(substr($colour_a_orig, 0, 2));
    $colour_a_orig_g = hexdec(substr($colour_a_orig, 2, 2));
    $colour_a_orig_b = hexdec(substr($colour_a_orig, 4, 2));
    $colour_a_new_r = hexdec(substr($colour_a_new, 0, 2));
    $colour_a_new_g = hexdec(substr($colour_a_new, 2, 2));
    $colour_a_new_b = hexdec(substr($colour_a_new, 4, 2));
    $colour_b1_orig_r = hexdec(substr($colour_b1_orig, 0, 2));
    $colour_b1_orig_g = hexdec(substr($colour_b1_orig, 2, 2));
    $colour_b1_orig_b = hexdec(substr($colour_b1_orig, 4, 2));
    $colour_b1_new_r = hexdec(substr($colour_b1_new, 0, 2));
    $colour_b1_new_g = hexdec(substr($colour_b1_new, 2, 2));
    $colour_b1_new_b = hexdec(substr($colour_b1_new, 4, 2));
    if (!is_null($colour_b2_new)) {
        $colour_b2_orig_r = hexdec(substr($colour_b2_orig, 0, 2));
        $colour_b2_orig_g = hexdec(substr($colour_b2_orig, 2, 2));
        $colour_b2_orig_b = hexdec(substr($colour_b2_orig, 4, 2));
        $colour_b2_new_r = hexdec(substr($colour_b2_new, 0, 2));
        $colour_b2_new_g = hexdec(substr($colour_b2_new, 2, 2));
        $colour_b2_new_b = hexdec(substr($colour_b2_new, 4, 2));
    }

    if (is_string($path)) {
        $_image = cms_imagecreatefrom($path);
        if ($_image === false) {
            warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($path)));
        }
    } else {
        $_image = $path;
    }
    $width = imagesx($_image);
    $height = imagesy($_image);
    if (function_exists('imageistruecolor')) {
        if (function_exists('imagecreatetruecolor')) {
            if (!imageistruecolor($_image)) {
                $image = imagecreatetruecolor($width, $height);

                imagealphablending($image, false);
                imagesavealpha($image, true);

                $transparent = imagecolortransparent($_image);
                if ($transparent >= imagecolorstotal($_image)) { // Workaround for corrupt images
                    $transparent = -1;
                }
                if ($transparent != -1) {
                    $_transparent = imagecolorsforindex($_image, $transparent);
                    imagecolortransparent($image, imagecolorallocate($image, $_transparent['red'], $_transparent['green'], $_transparent['blue']));
                }

                imagecopy($image, $_image, 0, 0, 0, 0, $width, $height);
            } else {
                $image = $_image;
                imagealphablending($image, false);
                imagesavealpha($image, true);
            }
        } else {
            $image = $_image;
            imagealphablending($image, false);
            imagesavealpha($image, true);
        }
    } else {
        $image = $_image;
        imagealphablending($image, false);
        imagesavealpha($image, true);
    }

    if (is_null($colour_b2_new)) {
        $colour_b_orig_r = $colour_b1_orig_r;
        $colour_b_orig_g = $colour_b1_orig_g;
        $colour_b_orig_b = $colour_b1_orig_b;
        $colour_b_orig = $colour_b1_orig;
        $colour_b_new_r = $colour_b1_new_r;
        $colour_b_new_g = $colour_b1_new_g;
        $colour_b_new_b = $colour_b1_new_b;
        $colour_b_new = $colour_b1_new;
    }

    if (function_exists('imageistruecolor')) {
        if (function_exists('imagecreatetruecolor')) {
            if (!imageistruecolor($_image)) {
                $trans_colour = imagecolortransparent($_image);
            } else {
                $trans_colour = null;
            }
        } else {
            $trans_colour = imagecolortransparent($_image);
        }
    } else {
        $trans_colour = imagecolortransparent($_image);
    }

    $gh = floatval($height - $gradient_offset);
    $gw = floatval($width - $gradient_offset);

    // Protect from a divide by zero, if images tampered with
    if ($gh == 0.0) {
        return $image;
    }
    if ($gw == 0.0) {
        return $image;
    }

    $vertical = ($gradient_direction == 'vertical');
    $horizontal = ($gradient_direction == 'horizontal');

    for ($y = 0; $y < $height; $y++) {
        $x = 0;
        $end = $width;
        if ($end_array) {
            if ((!is_null($pixel_x_start_array)) && (array_key_exists($y, $pixel_x_start_array))) {
                $end = min($width, $pixel_x_start_array[$y]);
            } else {
                $end = $width;
            }
        } else {
            if ((!is_null($pixel_x_start_array)) && (array_key_exists($y, $pixel_x_start_array))) {
                $x = $pixel_x_start_array[$y];
            }
        }
        for (; $x < $end; $x++) {
            $_existing_colour = imagecolorat($image, $x, $y);
            $existing_colour = imagecolorsforindex($image, $_existing_colour);
            if (!is_null($trans_colour)) {
                $__existing_colour = imagecolorat($_image, $x, $y);
                if ($__existing_colour == $trans_colour) {
                    $existing_colour['alpha'] = 127;
                }
            }

            if (!is_null($colour_b2_new)) {
                if ($vertical) {
                    $ratio = floatval($y - $gradient_offset) / $gh;
                } elseif ($horizontal) {
                    $ratio = floatval($x - $gradient_offset) / $gw;
                }
                $colour_b_orig_r = intval($colour_b1_orig_r + ($ratio * ($colour_b2_orig_r - $colour_b1_orig_r)));
                $colour_b_orig_g = intval($colour_b1_orig_g + ($ratio * ($colour_b2_orig_g - $colour_b1_orig_g)));
                $colour_b_orig_b = intval($colour_b1_orig_b + ($ratio * ($colour_b2_orig_b - $colour_b1_orig_b)));
                $colour_b_new_r = intval($colour_b1_new_r + ($ratio * ($colour_b2_new_r - $colour_b1_new_r)));
                $colour_b_new_g = intval($colour_b1_new_g + ($ratio * ($colour_b2_new_g - $colour_b1_new_g)));
                $colour_b_new_b = intval($colour_b1_new_b + ($ratio * ($colour_b2_new_b - $colour_b1_new_b)));
            }

            $existing_colour_r = $existing_colour['red'];
            $existing_colour_g = $existing_colour['green'];
            $existing_colour_b = $existing_colour['blue'];
            $existing_colour_a = $existing_colour['alpha'];

            $scale_r = null;
            $scale_g = null;
            $scale_b = null;
            $scale_count = 0;
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_r = ($colour_a_orig_r - $colour_b_orig_r == 0) ? 0.0 : floatval($existing_colour_r - $colour_b_orig_r) / floatval($colour_a_orig_r - $colour_b_orig_r);
                $scale_count++;
            }
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_g = ($colour_a_orig_g - $colour_b_orig_g == 0) ? 0.0 : floatval($existing_colour_g - $colour_b_orig_g) / floatval($colour_a_orig_g - $colour_b_orig_g);
                $scale_count++;
            }
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_b = ($colour_a_orig_b - $colour_b_orig_b == 0) ? 0.0 : floatval($existing_colour_b - $colour_b_orig_b) / floatval($colour_a_orig_b - $colour_b_orig_b);
                $scale_count++;
            }
            if ($scale_count == 0) { // Impossible to calculate
                $scale = 0.5;
            } else {
                $scale = ($scale_r + $scale_g + $scale_b) / floatval($scale_count);
            }

            $new_colour_r = fix_colour(intval(round(floatval($colour_a_new_r) * $scale + floatval($colour_b_new_r) * (1.0 - $scale))));
            $new_colour_g = fix_colour(intval(round(floatval($colour_a_new_g) * $scale + floatval($colour_b_new_g) * (1.0 - $scale))));
            $new_colour_b = fix_colour(intval(round(floatval($colour_a_new_b) * $scale + floatval($colour_b_new_b) * (1.0 - $scale))));

            if (function_exists('imagecolorallocatealpha')) {
                $target_colour = imagecolorallocatealpha($image, $new_colour_r, $new_colour_g, $new_colour_b, $existing_colour_a);
            } else {
                $target_colour = imagecolorallocate($image, $new_colour_r, $new_colour_g, $new_colour_b);
            }
            imagesetpixel($image, $x, $y, $target_colour);
        }
    }

    return $image;
}
