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