EpsImageBackEnd.php 12 KB

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