1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\captcha;
15
16class colour_manager
17{
18	var $img;
19	var $mode;
20	var $colours;
21	var $named_colours;
22
23	/**
24	* Create the colour manager, link it to the image resource
25	*/
26	function __construct($img, $background = false, $mode = 'ahsv')
27	{
28		$this->img = $img;
29		$this->mode = $mode;
30		$this->colours = array();
31		$this->named_colours = array();
32
33		if ($background !== false)
34		{
35			$bg = $this->allocate_named('background', $background);
36			imagefill($this->img, 0, 0, $bg);
37		}
38	}
39
40	/**
41	* Lookup a named colour resource
42	*/
43	function get_resource($named_colour)
44	{
45		if (isset($this->named_colours[$named_colour]))
46		{
47			return $this->named_colours[$named_colour];
48		}
49
50		if (isset($this->named_rgb[$named_colour]))
51		{
52			return $this->allocate_named($named_colour, $this->named_rgb[$named_colour], 'rgb');
53		}
54
55		return false;
56	}
57
58	/**
59	* Assign a name to a colour resource
60	*/
61	function name_colour($name, $resource)
62	{
63		$this->named_colours[$name] = $resource;
64	}
65
66	/**
67	* names and allocates a colour resource
68	*/
69	function allocate_named($name, $colour, $mode = false)
70	{
71		$resource = $this->allocate($colour, $mode);
72
73		if ($resource !== false)
74		{
75			$this->name_colour($name, $resource);
76		}
77		return $resource;
78	}
79
80	/**
81	* allocates a specified colour into the image
82	*/
83	function allocate($colour, $mode = false)
84	{
85		if ($mode === false)
86		{
87			$mode = $this->mode;
88		}
89
90		if (!is_array($colour))
91		{
92			if (isset($this->named_rgb[$colour]))
93			{
94				return $this->allocate_named($colour, $this->named_rgb[$colour], 'rgb');
95			}
96
97			if (!is_int($colour))
98			{
99				return false;
100			}
101
102			$mode = 'rgb';
103			$colour = array(255 & ($colour >> 16), 255 & ($colour >>  8), 255 & $colour);
104		}
105
106		if (isset($colour['mode']))
107		{
108			$mode = $colour['mode'];
109			unset($colour['mode']);
110		}
111
112		if (isset($colour['random']))
113		{
114			unset($colour['random']);
115			// everything else is params
116			return $this->random_colour($colour, $mode);
117		}
118
119		$rgb		= $this->model_convert($colour, $mode, 'rgb');
120		$store		= ($this->mode == 'rgb') ? $rgb : $this->model_convert($colour, $mode, $this->mode);
121		$resource	= imagecolorallocate($this->img, $rgb[0], $rgb[1], $rgb[2]);
122		$this->colours[$resource] = $store;
123
124		return $resource;
125	}
126
127	/**
128	* randomly generates a colour, with optional params
129	*/
130	function random_colour($params = array(), $mode = false)
131	{
132		if ($mode === false)
133		{
134			$mode = $this->mode;
135		}
136
137		switch ($mode)
138		{
139			case 'rgb':
140				// @TODO random rgb generation. do we intend to do this, or is it just too tedious?
141				break;
142
143			case 'ahsv':
144			case 'hsv':
145			default:
146
147				$default_params = array(
148					'hue_bias'			=> false,	// degree / 'r'/'g'/'b'/'c'/'m'/'y'   /'o'
149					'hue_range'			=> false,	// if hue bias, then difference range +/- from bias
150					'min_saturation'	=> 30,		// 0 - 100
151					'max_saturation'	=> 80,		// 0 - 100
152					'min_value'			=> 30,		// 0 - 100
153					'max_value'			=> 80,		// 0 - 100
154				);
155
156				$alt = ($mode == 'ahsv') ? true : false;
157				$params = array_merge($default_params, $params);
158
159				$min_hue		= 0;
160				$max_hue		= 359;
161				$min_saturation	= max(0, $params['min_saturation']);
162				$max_saturation	= min(100, $params['max_saturation']);
163				$min_value		= max(0, $params['min_value']);
164				$max_value		= min(100, $params['max_value']);
165
166				if ($params['hue_bias'] !== false)
167				{
168					if (is_numeric($params['hue_bias']))
169					{
170						$h = intval($params['hue_bias']) % 360;
171					}
172					else
173					{
174						switch ($params['hue_bias'])
175						{
176							case 'o':
177								$h = $alt ?  60 :  30;
178								break;
179
180							case 'y':
181								$h = $alt ? 120 :  60;
182								break;
183
184							case 'g':
185								$h = $alt ? 180 : 120;
186								break;
187
188							case 'c':
189								$h = $alt ? 210 : 180;
190								break;
191
192							case 'b':
193								$h = 240;
194								break;
195
196							case 'm':
197								$h = 300;
198								break;
199
200							case 'r':
201							default:
202								$h = 0;
203								break;
204						}
205					}
206
207					$min_hue = $h + 360;
208					$max_hue = $h + 360;
209
210					if ($params['hue_range'])
211					{
212						$min_hue -= min(180, $params['hue_range']);
213						$max_hue += min(180, $params['hue_range']);
214					}
215				}
216
217				$h = mt_rand($min_hue, $max_hue);
218				$s = mt_rand($min_saturation, $max_saturation);
219				$v = mt_rand($min_value, $max_value);
220
221				return $this->allocate(array($h, $s, $v), $mode);
222
223				break;
224		}
225	}
226
227	/**
228	*/
229	function colour_scheme($resource, $include_original = true)
230	{
231		$mode = 'hsv';
232
233		if (($pre = $this->get_resource($resource)) !== false)
234		{
235			$resource = $pre;
236		}
237
238		$colour = $this->model_convert($this->colours[$resource], $this->mode, $mode);
239		$results = ($include_original) ? array($resource) : array();
240		$colour2 = $colour3 = $colour4 = $colour;
241		$colour2[0] += 150;
242		$colour3[0] += 180;
243		$colour4[0] += 210;
244
245		$results[] = $this->allocate($colour2, $mode);
246		$results[] = $this->allocate($colour3, $mode);
247		$results[] = $this->allocate($colour4, $mode);
248
249		return $results;
250	}
251
252	/**
253	*/
254	function mono_range($resource, $count = 5, $include_original = true)
255	{
256		if (is_array($resource))
257		{
258			$results = array();
259			for ($i = 0, $size = count($resource); $i < $size; ++$i)
260			{
261				$results = array_merge($results, $this->mono_range($resource[$i], $count, $include_original));
262			}
263			return $results;
264		}
265
266		$mode = (in_array($this->mode, array('hsv', 'ahsv'), true) ? $this->mode : 'ahsv');
267		if (($pre = $this->get_resource($resource)) !== false)
268		{
269			$resource = $pre;
270		}
271
272		$colour = $this->model_convert($this->colours[$resource], $this->mode, $mode);
273
274		$results = array();
275		if ($include_original)
276		{
277			$results[] = $resource;
278			$count--;
279		}
280
281		// This is a hard problem. I chicken out and try to maintain readability at the cost of less randomness.
282
283		while ($count > 0)
284		{
285			$colour[1] = ($colour[1] + mt_rand(40,60)) % 99;
286			$colour[2] = ($colour[2] + mt_rand(40,60));
287			$results[] = $this->allocate($colour, $mode);
288			$count--;
289		}
290		return $results;
291	}
292
293	/**
294	* Convert from one colour model to another
295	*/
296	function model_convert($colour, $from_model, $to_model)
297	{
298		if ($from_model == $to_model)
299		{
300			return $colour;
301		}
302
303		switch ($to_model)
304		{
305			case 'hsv':
306
307				switch ($from_model)
308				{
309					case 'ahsv':
310						return $this->ah2h($colour);
311						break;
312
313					case 'rgb':
314						return $this->rgb2hsv($colour);
315						break;
316				}
317				break;
318
319			case 'ahsv':
320
321				switch ($from_model)
322				{
323					case 'hsv':
324						return $this->h2ah($colour);
325						break;
326
327					case 'rgb':
328						return $this->h2ah($this->rgb2hsv($colour));
329						break;
330				}
331				break;
332
333			case 'rgb':
334				switch ($from_model)
335				{
336					case 'hsv':
337						return $this->hsv2rgb($colour);
338						break;
339
340					case 'ahsv':
341						return $this->hsv2rgb($this->ah2h($colour));
342						break;
343				}
344				break;
345		}
346		return false;
347	}
348
349	/**
350	* Slightly altered from wikipedia's algorithm
351	*/
352	function hsv2rgb($hsv)
353	{
354		$this->normalize_hue($hsv[0]);
355
356		$h = $hsv[0];
357		$s = min(1, max(0, $hsv[1] / 100));
358		$v = min(1, max(0, $hsv[2] / 100));
359
360		// calculate hue sector
361		$hi = floor($hsv[0] / 60);
362
363		// calculate opposite colour
364		$p = $v * (1 - $s);
365
366		// calculate distance between hex vertices
367		$f = ($h / 60) - $hi;
368
369		// coming in or going out?
370		if (!($hi & 1))
371		{
372			$f = 1 - $f;
373		}
374
375		// calculate adjacent colour
376		$q = $v * (1 - ($f * $s));
377
378		switch ($hi)
379		{
380			case 0:
381				$rgb = array($v, $q, $p);
382				break;
383
384			case 1:
385				$rgb = array($q, $v, $p);
386				break;
387
388			case 2:
389				$rgb = array($p, $v, $q);
390				break;
391
392			case 3:
393				$rgb = array($p, $q, $v);
394				break;
395
396			case 4:
397				$rgb = array($q, $p, $v);
398				break;
399
400			case 5:
401				$rgb = array($v, $p, $q);
402				break;
403
404			default:
405				return array(0, 0, 0);
406				break;
407		}
408
409		return array(255 * $rgb[0], 255 * $rgb[1], 255 * $rgb[2]);
410	}
411
412	/**
413	* (more than) Slightly altered from wikipedia's algorithm
414	*/
415	function rgb2hsv($rgb)
416	{
417		$r = min(255, max(0, $rgb[0]));
418		$g = min(255, max(0, $rgb[1]));
419		$b = min(255, max(0, $rgb[2]));
420		$max = max($r, $g, $b);
421		$min = min($r, $g, $b);
422
423		$v = $max / 255;
424		$s = (!$max) ? 0 : 1 - ($min / $max);
425
426		// if max - min is 0, we want hue to be 0 anyway.
427		$h = $max - $min;
428
429		if ($h)
430		{
431			switch ($max)
432			{
433				case $g:
434					$h = 120 + (60 * ($b - $r) / $h);
435					break;
436
437				case $b:
438					$h = 240 + (60 * ($r - $g) / $h);
439					break;
440
441				case $r:
442					$h = 360 + (60 * ($g - $b) / $h);
443					break;
444			}
445		}
446		$this->normalize_hue($h);
447
448		return array($h, $s * 100, $v * 100);
449	}
450
451	/**
452	*/
453	function normalize_hue(&$hue)
454	{
455		$hue %= 360;
456
457		if ($hue < 0)
458		{
459			$hue += 360;
460		}
461	}
462
463	/**
464	* Alternate hue to hue
465	*/
466	function ah2h($ahue)
467	{
468		if (is_array($ahue))
469		{
470			$ahue[0] = $this->ah2h($ahue[0]);
471			return $ahue;
472		}
473		$this->normalize_hue($ahue);
474
475		// blue through red is already ok
476		if ($ahue >= 240)
477		{
478			return $ahue;
479		}
480
481		// ahue green is at 180
482		if ($ahue >= 180)
483		{
484			// return (240 - (2 * (240 - $ahue)));
485			return (2 * $ahue) - 240; // equivalent
486		}
487
488		// ahue yellow is at 120   (RYB rather than RGB)
489		if ($ahue >= 120)
490		{
491			return $ahue - 60;
492		}
493
494		return $ahue / 2;
495	}
496
497	/**
498	* hue to Alternate hue
499	*/
500	function h2ah($hue)
501	{
502		if (is_array($hue))
503		{
504			$hue[0] = $this->h2ah($hue[0]);
505			return $hue;
506		}
507		$this->normalize_hue($hue);
508
509		// blue through red is already ok
510		if ($hue >= 240)
511		{
512			return $hue;
513		}
514		else if ($hue <= 60)
515		{
516			return $hue * 2;
517		}
518		else if ($hue <= 120)
519		{
520			return $hue + 60;
521		}
522		else
523		{
524			return ($hue + 240) / 2;
525		}
526	}
527}
528