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