* @copyright 2022 smiley * @license MIT */ declare(strict_types=1); namespace chillerlan\QRCode\Output; use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf; /** * Encapsulated Postscript (EPS) output * * @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137 * @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf * @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf * @see https://github.com/chillerlan/php-qrcode/discussions/148 */ class QREps extends QROutputAbstract{ final public const MIME_TYPE = 'application/postscript'; public static function moduleValueIsValid(mixed $value):bool{ if(!is_array($value) || count($value) < 3){ return false; } // check the first values of the array foreach(array_values($value) as $i => $val){ if($i > 3){ break; } if(!is_numeric($val)){ return false; } } return true; } protected function prepareModuleValue(mixed $value):string{ $values = []; foreach(array_values($value) as $i => $val){ if($i > 3){ break; } // clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range $values[] = round((max(0, min(255, intval($val))) / 255), 6); } return $this->formatColor($values); } protected function getDefaultModuleValue(bool $isDark):string{ return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]); } /** * Set the color format string * * 4 values in the color array will be interpreted as CMYK, 3 as RGB * * @throws \chillerlan\QRCode\Output\QRCodeOutputException * * @param float[] $values */ protected function formatColor(array $values):string{ $count = count($values); if($count < 3){ throw new QRCodeOutputException('invalid color value'); } // the EPS functions "C" and "R" are defined in the header below $format = ($count === 4) ? '%f %f %f %f C' // CMYK : '%f %f %f R'; // RGB return sprintf($format, ...$values); } public function dump(string|null $file = null):string{ $eps = implode("\n", [ // initialize header $this->header(), // create the path elements $this->paths(), // end file '%%EOF', ]); $this->saveToFile($eps, $file); return $eps; } /** * Returns the main header for the EPS file, including function definitions and background */ protected function header():string{ [$width, $height] = $this->getOutputDimensions(); $header = [ // main header '%!PS-Adobe-3.0 EPSF-3.0', '%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)', '%%Title: QR Code', sprintf('%%%%CreationDate: %1$s', date('c')), '%%DocumentData: Clean7Bit', '%%LanguageLevel: 3', sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height), '%%EndComments', // function definitions '%%BeginProlog', '/F { rectfill } def', '/R { setrgbcolor } def', '/C { setcmykcolor } def', '%%EndProlog', ]; if($this::moduleValueIsValid($this->options->bgColor)){ $header[] = $this->prepareModuleValue($this->options->bgColor); $header[] = sprintf('0 0 %s %s F', $width, $height); } return implode("\n", $header); } /** * returns one or more EPS path blocks */ protected function paths():string{ $paths = $this->collectModules(); $eps = []; foreach($paths as $M_TYPE => $path){ if($path === []){ continue; } $eps[] = $this->getModuleValue($M_TYPE); $eps[] = implode("\n", $path); } return implode("\n", $eps); } /** * Returns a path segment for a single module */ protected function moduleTransform(int $x, int $y, int $M_TYPE, int $M_TYPE_LAYER):string|null{ if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ return null; } $outputX = ($x * $this->scale); // Actual size - one block = Topmost y pos. $top = ($this->length - $this->scale); // Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here $outputY = ($top - ($y * $this->scale)); return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale); } }