1<?php
2
3namespace Mpdf\Color;
4
5use Mpdf\Mpdf;
6
7class ColorConverter
8{
9
10	const MODE_GRAYSCALE = 1;
11
12	const MODE_SPOT = 2;
13
14	const MODE_RGB = 3;
15
16	const MODE_CMYK = 4;
17
18	const MODE_RGBA = 5;
19
20	const MODE_CMYKA = 6;
21
22	private $mpdf;
23
24	private $colorModeConverter;
25
26	private $colorSpaceRestrictor;
27
28	private $cache;
29
30	public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter, ColorSpaceRestrictor $colorSpaceRestrictor)
31	{
32		$this->mpdf = $mpdf;
33		$this->colorModeConverter = $colorModeConverter;
34		$this->colorSpaceRestrictor = $colorSpaceRestrictor;
35
36		$this->cache = [];
37	}
38
39	public function convert($color, array &$PDFAXwarnings = [])
40	{
41		$color = strtolower(trim($color));
42
43		if ($color === 'transparent' || $color === 'inherit') {
44			return false;
45		}
46
47		if (isset(NamedColors::$colors[$color])) {
48			$color = NamedColors::$colors[$color];
49		}
50
51		if (!isset($this->cache[$color])) {
52			$c = $this->convertPlain($color, $PDFAXwarnings);
53			$cstr = '';
54			if (is_array($c)) {
55				$c = array_pad($c, 6, 0);
56				$cstr = pack(
57					'a1ccccc',
58					$c[0],
59					round($c[1]) & 0xFF,
60					round($c[2]) & 0xFF,
61					round($c[3]) & 0xFF,
62					round($c[4]) & 0xFF,
63					round($c[5]) & 0xFF
64				);
65			}
66
67			$this->cache[$color] = $cstr;
68		}
69
70		return $this->cache[$color];
71	}
72
73	public function lighten($c)
74	{
75		$this->ensureBinaryColorFormat($c);
76
77		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
78			list($h, $s, $l) = $this->colorModeConverter->rgb2hsl(ord($c[1]) / 255, ord($c[2]) / 255, ord($c[3]) / 255);
79			$l += ((1 - $l) * 0.8);
80			list($r, $g, $b) = $this->colorModeConverter->hsl2rgb($h, $s, $l);
81			$ret = [3, $r, $g, $b];
82		} elseif ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
83			$ret = [4, max(0, ord($c[1]) - 20), max(0, ord($c[2]) - 20), max(0, ord($c[3]) - 20), max(0, ord($c[4]) - 20)];
84		} elseif ($c[0] == static::MODE_GRAYSCALE) {
85			$ret = [1, min(255, ord($c[1]) + 32)];
86		}
87
88		$c = array_pad($ret, 6, 0);
89		$cstr = pack(
90			'a1ccccc',
91			$c[0],
92			round($c[1]) & 0xFF,
93			round($c[2]) & 0xFF,
94			round($c[3]) & 0xFF,
95			round($c[4]) & 0xFF,
96			round($c[5]) & 0xFF
97		);
98
99		return $cstr;
100	}
101
102	public function darken($c)
103	{
104		$this->ensureBinaryColorFormat($c);
105
106		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
107			list($h, $s, $l) = $this->colorModeConverter->rgb2hsl(ord($c[1]) / 255, ord($c[2]) / 255, ord($c[3]) / 255);
108			$s *= 0.25;
109			$l *= 0.75;
110			list($r, $g, $b) = $this->colorModeConverter->hsl2rgb($h, $s, $l);
111			$ret = [3, $r, $g, $b];
112		} elseif ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
113			$ret = [4, min(100, ord($c[1]) + 20), min(100, ord($c[2]) + 20), min(100, ord($c[3]) + 20), min(100, ord($c[4]) + 20)];
114		} elseif ($c[0] == static::MODE_GRAYSCALE) {
115			$ret = [1, max(0, ord($c[1]) - 32)];
116		}
117		$c = array_pad($ret, 6, 0);
118		$cstr = pack('a1ccccc', $c[0], $c[1] & 0xFF, $c[2] & 0xFF, $c[3] & 0xFF, $c[4] & 0xFF, $c[5] & 0xFF);
119
120		return $cstr;
121	}
122
123	/**
124	 * @param string $c
125	 * @return float[]
126	 */
127	public function invert($c)
128	{
129		$this->ensureBinaryColorFormat($c);
130
131		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
132			return [3, 255 - ord($c[1]), 255 - ord($c[2]), 255 - ord($c[3])];
133		}
134
135		if ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
136			return [4, 100 - ord($c[1]), 100 - ord($c[2]), 100 - ord($c[3]), 100 - ord($c[4])];
137		}
138
139		if ($c[0] == static::MODE_GRAYSCALE) {
140			return [1, 255 - ord($c[1])];
141		}
142
143		// Cannot cope with non-RGB colors at present
144		throw new \Mpdf\MpdfException('Trying to invert non-RGB color');
145	}
146
147	/**
148	 * @param string $c Binary color string
149	 *
150	 * @return string
151	 */
152	public function colAtoString($c)
153	{
154		if ($c[0] == static::MODE_GRAYSCALE) {
155			return 'rgb(' . ord($c[1]) . ', ' . ord($c[1]) . ', ' . ord($c[1]) . ')';
156		}
157
158		if ($c[0] == static::MODE_SPOT) {
159			return 'spot(' . ord($c[1]) . ', ' . ord($c[2]) . ')';
160		}
161
162		if ($c[0] == static::MODE_RGB) {
163			return 'rgb(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ')';
164		}
165
166		if ($c[0] == static::MODE_CMYK) {
167			return 'cmyk(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . ord($c[4]) . ')';
168		}
169
170		if ($c[0] == static::MODE_RGBA) {
171			return 'rgba(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . sprintf('%0.2F', ord($c[4]) / 100) . ')';
172		}
173
174		if ($c[0] == static::MODE_CMYKA) {
175			return 'cmyka(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . ord($c[4]) . ', ' . sprintf('%0.2F', ord($c[5]) / 100) . ')';
176		}
177
178		return '';
179	}
180
181	/**
182	 * @param string $color
183	 * @param string[] $PDFAXwarnings
184	 *
185	 * @return bool|float[]
186	 */
187	private function convertPlain($color, array &$PDFAXwarnings = [])
188	{
189		$c = false;
190
191		if (preg_match('/^[\d]+$/', $color)) {
192			$c = [static::MODE_GRAYSCALE, $color]; // i.e. integer only
193		} elseif (strpos($color, '#') === 0) { // case of #nnnnnn or #nnn
194			$c = $this->processHashColor($color);
195		} elseif (preg_match('/(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\((.*?)\)/', $color, $m)) {
196			$c = $this->processModeColor($m[1], explode(',', $m[2]));
197		}
198
199		if ($this->mpdf->PDFA || $this->mpdf->PDFX || $this->mpdf->restrictColorSpace) {
200			$c = $this->restrictColorSpace($c, $color, $PDFAXwarnings);
201		}
202
203		return $c;
204	}
205
206	/**
207	 * @param string $color
208	 *
209	 * @return float[]
210	 */
211	private function processHashColor($color)
212	{
213		// in case of Background: #CCC url() x-repeat etc.
214		$cor = preg_replace('/\s+.*/', '', $color);
215
216		// Turn #RGB into #RRGGBB
217		if (strlen($cor) === 4) {
218			$cor = '#' . $cor[1] . $cor[1] . $cor[2] . $cor[2] . $cor[3] . $cor[3];
219		}
220
221		$r = self::safeHexDec(substr($cor, 1, 2));
222		$g = self::safeHexDec(substr($cor, 3, 2));
223		$b = self::safeHexDec(substr($cor, 5, 2));
224
225		return [3, $r, $g, $b];
226	}
227
228	/**
229	 * @param $mode
230	 * @param mixed[] $cores
231	 * @return bool|float[]
232	 */
233	private function processModeColor($mode, array $cores)
234	{
235		$c = false;
236
237		$cores = $this->convertPercentCoreValues($mode, $cores);
238
239		switch ($mode) {
240			case 'rgb':
241				return [static::MODE_RGB, $cores[0], $cores[1], $cores[2]];
242
243			case 'rgba':
244				return [static::MODE_RGBA, $cores[0], $cores[1], $cores[2], $cores[3] * 100];
245
246			case 'cmyk':
247			case 'device-cmyk':
248				return [static::MODE_CMYK, $cores[0], $cores[1], $cores[2], $cores[3]];
249
250			case 'cmyka':
251			case 'device-cmyka':
252				return [static::MODE_CMYKA, $cores[0], $cores[1], $cores[2], $cores[3], $cores[4] * 100];
253
254			case 'hsl':
255				$conv = $this->colorModeConverter->hsl2rgb($cores[0] / 360, $cores[1], $cores[2]);
256				return [static::MODE_RGB, $conv[0], $conv[1], $conv[2]];
257
258			case 'hsla':
259				$conv = $this->colorModeConverter->hsl2rgb($cores[0] / 360, $cores[1], $cores[2]);
260				return [static::MODE_RGBA, $conv[0], $conv[1], $conv[2], $cores[3] * 100];
261
262			case 'spot':
263				$name = strtoupper(trim($cores[0]));
264
265				if (!isset($this->mpdf->spotColors[$name])) {
266					if (isset($cores[5])) {
267						$this->mpdf->AddSpotColor($cores[0], $cores[2], $cores[3], $cores[4], $cores[5]);
268					} else {
269						throw new \Mpdf\MpdfException(sprintf('Undefined spot color "%s"', $name));
270					}
271				}
272
273				return [static::MODE_SPOT, $this->mpdf->spotColors[$name]['i'], $cores[1]];
274		}
275
276		return $c;
277	}
278
279	/**
280	 * @param string $mode
281	 * @param mixed[] $cores
282	 *
283	 * @return float[]
284	 */
285	private function convertPercentCoreValues($mode, array $cores)
286	{
287		$ncores = count($cores);
288
289		if (strpos($cores[0], '%') !== false) {
290			$cores[0] = (float) $cores[0];
291			if ($mode === 'rgb' || $mode === 'rgba') {
292				$cores[0] = (int) ($cores[0] * 255 / 100);
293			}
294		}
295
296		if ($ncores > 1 && strpos($cores[1], '%') !== false) {
297			$cores[1] = (float) $cores[1];
298			if ($mode === 'rgb' || $mode === 'rgba') {
299				$cores[1] = (int) ($cores[1] * 255 / 100);
300			}
301			if ($mode === 'hsl' || $mode === 'hsla') {
302				$cores[1] /= 100;
303			}
304		}
305
306		if ($ncores > 2 && strpos($cores[2], '%') !== false) {
307			$cores[2] = (float) $cores[2];
308			if ($mode === 'rgb' || $mode === 'rgba') {
309				$cores[2] = (int) ($cores[2] * 255 / 100);
310			}
311			if ($mode === 'hsl' || $mode === 'hsla') {
312				$cores[2] /= 100;
313			}
314		}
315
316		if ($ncores > 3 && strpos($cores[3], '%') !== false) {
317			$cores[3] = (float) $cores[3];
318		}
319
320		return $cores;
321	}
322
323	/**
324	 * @param mixed $c
325	 * @param string $color
326	 * @param string[] $PDFAXwarnings
327	 *
328	 * @return float[]
329	 */
330	private function restrictColorSpace($c, $color, &$PDFAXwarnings = [])
331	{
332		return $this->colorSpaceRestrictor->restrictColorSpace($c, $color, $PDFAXwarnings);
333	}
334
335	/**
336	 * @param string $color Binary color string
337	 */
338	private function ensureBinaryColorFormat($color)
339	{
340		if (!is_string($color)) {
341			throw new \Mpdf\MpdfException('Invalid color input, binary color string expected');
342		}
343
344		if (strlen($color) !== 6) {
345			throw new \Mpdf\MpdfException('Invalid color input, binary color string expected');
346		}
347
348		if (!in_array($color[0], [static::MODE_GRAYSCALE, static::MODE_SPOT, static::MODE_RGB, static::MODE_CMYK, static::MODE_RGBA, static::MODE_CMYKA])) {
349			throw new \Mpdf\MpdfException('Invalid color input, invalid color mode in binary color string');
350		}
351	}
352
353	/**
354	 * Converts the given hexString to its decimal representation when all digits are hexadecimal
355	 *
356	 * @param string $hexString The hexadecimal string to convert
357	 * @return float|int The decimal representation of hexString or 0 if not all digits of hexString are hexadecimal
358	 */
359	private function safeHexDec($hexString)
360	{
361		return ctype_xdigit($hexString) ? hexdec($hexString) : 0;
362	}
363}
364