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