QREps.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. /**
  3. * Class QREps
  4. *
  5. * @created 09.05.2022
  6. * @author smiley <smiley@chillerlan.net>
  7. * @copyright 2022 smiley
  8. * @license MIT
  9. */
  10. declare(strict_types=1);
  11. namespace chillerlan\QRCode\Output;
  12. use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf;
  13. /**
  14. * Encapsulated Postscript (EPS) output
  15. *
  16. * @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137
  17. * @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf
  18. * @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
  19. * @see https://github.com/chillerlan/php-qrcode/discussions/148
  20. */
  21. class QREps extends QROutputAbstract{
  22. final public const MIME_TYPE = 'application/postscript';
  23. public static function moduleValueIsValid(mixed $value):bool{
  24. if(!is_array($value) || count($value) < 3){
  25. return false;
  26. }
  27. // check the first values of the array
  28. foreach(array_values($value) as $i => $val){
  29. if($i > 3){
  30. break;
  31. }
  32. if(!is_numeric($val)){
  33. return false;
  34. }
  35. }
  36. return true;
  37. }
  38. protected function prepareModuleValue(mixed $value):string{
  39. $values = [];
  40. foreach(array_values($value) as $i => $val){
  41. if($i > 3){
  42. break;
  43. }
  44. // clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range
  45. $values[] = round((max(0, min(255, intval($val))) / 255), 6);
  46. }
  47. return $this->formatColor($values);
  48. }
  49. protected function getDefaultModuleValue(bool $isDark):string{
  50. return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]);
  51. }
  52. /**
  53. * Set the color format string
  54. *
  55. * 4 values in the color array will be interpreted as CMYK, 3 as RGB
  56. *
  57. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  58. *
  59. * @param float[] $values
  60. */
  61. protected function formatColor(array $values):string{
  62. $count = count($values);
  63. if($count < 3){
  64. throw new QRCodeOutputException('invalid color value');
  65. }
  66. // the EPS functions "C" and "R" are defined in the header below
  67. $format = ($count === 4)
  68. ? '%f %f %f %f C' // CMYK
  69. : '%f %f %f R'; // RGB
  70. return sprintf($format, ...$values);
  71. }
  72. public function dump(string|null $file = null):string{
  73. $eps = implode("\n", [
  74. // initialize header
  75. $this->header(),
  76. // create the path elements
  77. $this->paths(),
  78. // end file
  79. '%%EOF',
  80. ]);
  81. $this->saveToFile($eps, $file);
  82. return $eps;
  83. }
  84. /**
  85. * Returns the main header for the EPS file, including function definitions and background
  86. */
  87. protected function header():string{
  88. [$width, $height] = $this->getOutputDimensions();
  89. $header = [
  90. // main header
  91. '%!PS-Adobe-3.0 EPSF-3.0',
  92. '%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)',
  93. '%%Title: QR Code',
  94. sprintf('%%%%CreationDate: %1$s', date('c')),
  95. '%%DocumentData: Clean7Bit',
  96. '%%LanguageLevel: 3',
  97. sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height),
  98. '%%EndComments',
  99. // function definitions
  100. '%%BeginProlog',
  101. '/F { rectfill } def',
  102. '/R { setrgbcolor } def',
  103. '/C { setcmykcolor } def',
  104. '%%EndProlog',
  105. ];
  106. if($this::moduleValueIsValid($this->options->bgColor)){
  107. $header[] = $this->prepareModuleValue($this->options->bgColor);
  108. $header[] = sprintf('0 0 %s %s F', $width, $height);
  109. }
  110. return implode("\n", $header);
  111. }
  112. /**
  113. * returns one or more EPS path blocks
  114. */
  115. protected function paths():string{
  116. $paths = $this->collectModules();
  117. $eps = [];
  118. foreach($paths as $M_TYPE => $path){
  119. if($path === []){
  120. continue;
  121. }
  122. $eps[] = $this->getModuleValue($M_TYPE);
  123. $eps[] = implode("\n", $path);
  124. }
  125. return implode("\n", $eps);
  126. }
  127. /**
  128. * Returns a path segment for a single module
  129. */
  130. protected function moduleTransform(int $x, int $y, int $M_TYPE, int $M_TYPE_LAYER):string|null{
  131. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  132. return null;
  133. }
  134. $outputX = ($x * $this->scale);
  135. // Actual size - one block = Topmost y pos.
  136. $top = ($this->length - $this->scale);
  137. // Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here
  138. $outputY = ($top - ($y * $this->scale));
  139. return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale);
  140. }
  141. }