1<?php 2declare(strict_types = 1); 3 4namespace BaconQrCode\Renderer\Image; 5 6use BaconQrCode\Exception\RuntimeException; 7use BaconQrCode\Renderer\Color\Alpha; 8use BaconQrCode\Renderer\Color\Cmyk; 9use BaconQrCode\Renderer\Color\ColorInterface; 10use BaconQrCode\Renderer\Color\Gray; 11use BaconQrCode\Renderer\Color\Rgb; 12use BaconQrCode\Renderer\Path\Close; 13use BaconQrCode\Renderer\Path\Curve; 14use BaconQrCode\Renderer\Path\EllipticArc; 15use BaconQrCode\Renderer\Path\Line; 16use BaconQrCode\Renderer\Path\Move; 17use BaconQrCode\Renderer\Path\Path; 18use BaconQrCode\Renderer\RendererStyle\Gradient; 19use BaconQrCode\Renderer\RendererStyle\GradientType; 20 21final class EpsImageBackEnd implements ImageBackEndInterface 22{ 23 private const PRECISION = 3; 24 25 /** 26 * @var string|null 27 */ 28 private $eps; 29 30 public function new(int $size, ColorInterface $backgroundColor) : void 31 { 32 $this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n" 33 . "%%Creator: BaconQrCode\n" 34 . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size) 35 . "%%BeginProlog\n" 36 . "save\n" 37 . "50 dict begin\n" 38 . "/q { gsave } bind def\n" 39 . "/Q { grestore } bind def\n" 40 . "/s { scale } bind def\n" 41 . "/t { translate } bind def\n" 42 . "/r { rotate } bind def\n" 43 . "/n { newpath } bind def\n" 44 . "/m { moveto } bind def\n" 45 . "/l { lineto } bind def\n" 46 . "/c { curveto } bind def\n" 47 . "/z { closepath } bind def\n" 48 . "/f { eofill } bind def\n" 49 . "/rgb { setrgbcolor } bind def\n" 50 . "/cmyk { setcmykcolor } bind def\n" 51 . "/gray { setgray } bind def\n" 52 . "%%EndProlog\n" 53 . "1 -1 s\n" 54 . sprintf("0 -%d t\n", $size); 55 56 if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) { 57 return; 58 } 59 60 $this->eps .= wordwrap( 61 '0 0 m' 62 . sprintf(' %s 0 l', (string) $size) 63 . sprintf(' %s %s l', (string) $size, (string) $size) 64 . sprintf(' 0 %s l', (string) $size) 65 . ' z' 66 . ' ' .$this->getColorSetString($backgroundColor) . " f\n", 67 75, 68 "\n " 69 ); 70 } 71 72 public function scale(float $size) : void 73 { 74 if (null === $this->eps) { 75 throw new RuntimeException('No image has been started'); 76 } 77 78 $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION)); 79 } 80 81 public function translate(float $x, float $y) : void 82 { 83 if (null === $this->eps) { 84 throw new RuntimeException('No image has been started'); 85 } 86 87 $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION)); 88 } 89 90 public function rotate(int $degrees) : void 91 { 92 if (null === $this->eps) { 93 throw new RuntimeException('No image has been started'); 94 } 95 96 $this->eps .= sprintf("%d r\n", $degrees); 97 } 98 99 public function push() : void 100 { 101 if (null === $this->eps) { 102 throw new RuntimeException('No image has been started'); 103 } 104 105 $this->eps .= "q\n"; 106 } 107 108 public function pop() : void 109 { 110 if (null === $this->eps) { 111 throw new RuntimeException('No image has been started'); 112 } 113 114 $this->eps .= "Q\n"; 115 } 116 117 public function drawPathWithColor(Path $path, ColorInterface $color) : void 118 { 119 if (null === $this->eps) { 120 throw new RuntimeException('No image has been started'); 121 } 122 123 $fromX = 0; 124 $fromY = 0; 125 $this->eps .= wordwrap( 126 'n ' 127 . $this->drawPathOperations($path, $fromX, $fromY) 128 . ' ' . $this->getColorSetString($color) . " f\n", 129 75, 130 "\n " 131 ); 132 } 133 134 public function drawPathWithGradient( 135 Path $path, 136 Gradient $gradient, 137 float $x, 138 float $y, 139 float $width, 140 float $height 141 ) : void { 142 if (null === $this->eps) { 143 throw new RuntimeException('No image has been started'); 144 } 145 146 $fromX = 0; 147 $fromY = 0; 148 $this->eps .= wordwrap( 149 'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n", 150 75, 151 "\n " 152 ); 153 154 $this->createGradientFill($gradient, $x, $y, $width, $height); 155 } 156 157 public function done() : string 158 { 159 if (null === $this->eps) { 160 throw new RuntimeException('No image has been started'); 161 } 162 163 $this->eps .= "%%TRAILER\nend restore\n%%EOF"; 164 $blob = $this->eps; 165 $this->eps = null; 166 167 return $blob; 168 } 169 170 private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string 171 { 172 $pathData = []; 173 174 foreach ($ops as $op) { 175 switch (true) { 176 case $op instanceof Move: 177 $fromX = $toX = round($op->getX(), self::PRECISION); 178 $fromY = $toY = round($op->getY(), self::PRECISION); 179 $pathData[] = sprintf('%s %s m', $toX, $toY); 180 break; 181 182 case $op instanceof Line: 183 $fromX = $toX = round($op->getX(), self::PRECISION); 184 $fromY = $toY = round($op->getY(), self::PRECISION); 185 $pathData[] = sprintf('%s %s l', $toX, $toY); 186 break; 187 188 case $op instanceof EllipticArc: 189 $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY); 190 break; 191 192 case $op instanceof Curve: 193 $x1 = round($op->getX1(), self::PRECISION); 194 $y1 = round($op->getY1(), self::PRECISION); 195 $x2 = round($op->getX2(), self::PRECISION); 196 $y2 = round($op->getY2(), self::PRECISION); 197 $fromX = $x3 = round($op->getX3(), self::PRECISION); 198 $fromY = $y3 = round($op->getY3(), self::PRECISION); 199 $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3); 200 break; 201 202 case $op instanceof Close: 203 $pathData[] = 'z'; 204 break; 205 206 default: 207 throw new RuntimeException('Unexpected draw operation: ' . get_class($op)); 208 } 209 } 210 211 return implode(' ', $pathData); 212 } 213 214 private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void 215 { 216 $startColor = $gradient->getStartColor(); 217 $endColor = $gradient->getEndColor(); 218 219 if ($startColor instanceof Alpha) { 220 $startColor = $startColor->getBaseColor(); 221 } 222 223 $startColorType = get_class($startColor); 224 225 if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) { 226 $startColorType = Cmyk::class; 227 $startColor = $startColor->toCmyk(); 228 } 229 230 if (get_class($endColor) !== $startColorType) { 231 switch ($startColorType) { 232 case Cmyk::class: 233 $endColor = $endColor->toCmyk(); 234 break; 235 236 case Rgb::class: 237 $endColor = $endColor->toRgb(); 238 break; 239 240 case Gray::class: 241 $endColor = $endColor->toGray(); 242 break; 243 } 244 } 245 246 $this->eps .= "eoclip\n<<\n"; 247 248 if ($gradient->getType() === GradientType::RADIAL()) { 249 $this->eps .= " /ShadingType 3\n"; 250 } else { 251 $this->eps .= " /ShadingType 2\n"; 252 } 253 254 $this->eps .= " /Extend [ true true ]\n" 255 . " /AntiAlias true\n"; 256 257 switch ($startColorType) { 258 case Cmyk::class: 259 $this->eps .= " /ColorSpace /DeviceCMYK\n"; 260 break; 261 262 case Rgb::class: 263 $this->eps .= " /ColorSpace /DeviceRGB\n"; 264 break; 265 266 case Gray::class: 267 $this->eps .= " /ColorSpace /DeviceGray\n"; 268 break; 269 } 270 271 switch ($gradient->getType()) { 272 case GradientType::HORIZONTAL(): 273 $this->eps .= sprintf( 274 " /Coords [ %s %s %s %s ]\n", 275 round($x, self::PRECISION), 276 round($y, self::PRECISION), 277 round($x + $width, self::PRECISION), 278 round($y, self::PRECISION) 279 ); 280 break; 281 282 case GradientType::VERTICAL(): 283 $this->eps .= sprintf( 284 " /Coords [ %s %s %s %s ]\n", 285 round($x, self::PRECISION), 286 round($y, self::PRECISION), 287 round($x, self::PRECISION), 288 round($y + $height, self::PRECISION) 289 ); 290 break; 291 292 case GradientType::DIAGONAL(): 293 $this->eps .= sprintf( 294 " /Coords [ %s %s %s %s ]\n", 295 round($x, self::PRECISION), 296 round($y, self::PRECISION), 297 round($x + $width, self::PRECISION), 298 round($y + $height, self::PRECISION) 299 ); 300 break; 301 302 case GradientType::INVERSE_DIAGONAL(): 303 $this->eps .= sprintf( 304 " /Coords [ %s %s %s %s ]\n", 305 round($x, self::PRECISION), 306 round($y + $height, self::PRECISION), 307 round($x + $width, self::PRECISION), 308 round($y, self::PRECISION) 309 ); 310 break; 311 312 case GradientType::RADIAL(): 313 $centerX = ($x + $width) / 2; 314 $centerY = ($y + $height) / 2; 315 316 $this->eps .= sprintf( 317 " /Coords [ %s %s 0 %s %s %s ]\n", 318 round($centerX, self::PRECISION), 319 round($centerY, self::PRECISION), 320 round($centerX, self::PRECISION), 321 round($centerY, self::PRECISION), 322 round(max($width, $height) / 2, self::PRECISION) 323 ); 324 break; 325 } 326 327 $this->eps .= " /Function\n" 328 . " <<\n" 329 . " /FunctionType 2\n" 330 . " /Domain [ 0 1 ]\n" 331 . sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor)) 332 . sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor)) 333 . " /N 1\n" 334 . " >>\n>>\nshfill\nQ\n"; 335 } 336 337 private function getColorSetString(ColorInterface $color) : string 338 { 339 if ($color instanceof Rgb) { 340 return $this->getColorString($color) . ' rgb'; 341 } 342 343 if ($color instanceof Cmyk) { 344 return $this->getColorString($color) . ' cmyk'; 345 } 346 347 if ($color instanceof Gray) { 348 return $this->getColorString($color) . ' gray'; 349 } 350 351 return $this->getColorSetString($color->toCmyk()); 352 } 353 354 private function getColorString(ColorInterface $color) : string 355 { 356 if ($color instanceof Rgb) { 357 return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255); 358 } 359 360 if ($color instanceof Cmyk) { 361 return sprintf( 362 '%s %s %s %s', 363 $color->getCyan() / 100, 364 $color->getMagenta() / 100, 365 $color->getYellow() / 100, 366 $color->getBlack() / 100 367 ); 368 } 369 370 if ($color instanceof Gray) { 371 return sprintf('%s', $color->getGray() / 100); 372 } 373 374 return $this->getColorString($color->toCmyk()); 375 } 376} 377