You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
12 KiB
376 lines
12 KiB
<?php |
|
declare(strict_types = 1); |
|
|
|
namespace BaconQrCode\Renderer\Image; |
|
|
|
use BaconQrCode\Exception\RuntimeException; |
|
use BaconQrCode\Renderer\Color\Alpha; |
|
use BaconQrCode\Renderer\Color\Cmyk; |
|
use BaconQrCode\Renderer\Color\ColorInterface; |
|
use BaconQrCode\Renderer\Color\Gray; |
|
use BaconQrCode\Renderer\Color\Rgb; |
|
use BaconQrCode\Renderer\Path\Close; |
|
use BaconQrCode\Renderer\Path\Curve; |
|
use BaconQrCode\Renderer\Path\EllipticArc; |
|
use BaconQrCode\Renderer\Path\Line; |
|
use BaconQrCode\Renderer\Path\Move; |
|
use BaconQrCode\Renderer\Path\Path; |
|
use BaconQrCode\Renderer\RendererStyle\Gradient; |
|
use BaconQrCode\Renderer\RendererStyle\GradientType; |
|
|
|
final class EpsImageBackEnd implements ImageBackEndInterface |
|
{ |
|
private const PRECISION = 3; |
|
|
|
/** |
|
* @var string|null |
|
*/ |
|
private $eps; |
|
|
|
public function new(int $size, ColorInterface $backgroundColor) : void |
|
{ |
|
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n" |
|
. "%%Creator: BaconQrCode\n" |
|
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size) |
|
. "%%BeginProlog\n" |
|
. "save\n" |
|
. "50 dict begin\n" |
|
. "/q { gsave } bind def\n" |
|
. "/Q { grestore } bind def\n" |
|
. "/s { scale } bind def\n" |
|
. "/t { translate } bind def\n" |
|
. "/r { rotate } bind def\n" |
|
. "/n { newpath } bind def\n" |
|
. "/m { moveto } bind def\n" |
|
. "/l { lineto } bind def\n" |
|
. "/c { curveto } bind def\n" |
|
. "/z { closepath } bind def\n" |
|
. "/f { eofill } bind def\n" |
|
. "/rgb { setrgbcolor } bind def\n" |
|
. "/cmyk { setcmykcolor } bind def\n" |
|
. "/gray { setgray } bind def\n" |
|
. "%%EndProlog\n" |
|
. "1 -1 s\n" |
|
. sprintf("0 -%d t\n", $size); |
|
|
|
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) { |
|
return; |
|
} |
|
|
|
$this->eps .= wordwrap( |
|
'0 0 m' |
|
. sprintf(' %s 0 l', (string) $size) |
|
. sprintf(' %s %s l', (string) $size, (string) $size) |
|
. sprintf(' 0 %s l', (string) $size) |
|
. ' z' |
|
. ' ' .$this->getColorSetString($backgroundColor) . " f\n", |
|
75, |
|
"\n " |
|
); |
|
} |
|
|
|
public function scale(float $size) : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION)); |
|
} |
|
|
|
public function translate(float $x, float $y) : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION)); |
|
} |
|
|
|
public function rotate(int $degrees) : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= sprintf("%d r\n", $degrees); |
|
} |
|
|
|
public function push() : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= "q\n"; |
|
} |
|
|
|
public function pop() : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= "Q\n"; |
|
} |
|
|
|
public function drawPathWithColor(Path $path, ColorInterface $color) : void |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$fromX = 0; |
|
$fromY = 0; |
|
$this->eps .= wordwrap( |
|
'n ' |
|
. $this->drawPathOperations($path, $fromX, $fromY) |
|
. ' ' . $this->getColorSetString($color) . " f\n", |
|
75, |
|
"\n " |
|
); |
|
} |
|
|
|
public function drawPathWithGradient( |
|
Path $path, |
|
Gradient $gradient, |
|
float $x, |
|
float $y, |
|
float $width, |
|
float $height |
|
) : void { |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$fromX = 0; |
|
$fromY = 0; |
|
$this->eps .= wordwrap( |
|
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n", |
|
75, |
|
"\n " |
|
); |
|
|
|
$this->createGradientFill($gradient, $x, $y, $width, $height); |
|
} |
|
|
|
public function done() : string |
|
{ |
|
if (null === $this->eps) { |
|
throw new RuntimeException('No image has been started'); |
|
} |
|
|
|
$this->eps .= "%%TRAILER\nend restore\n%%EOF"; |
|
$blob = $this->eps; |
|
$this->eps = null; |
|
|
|
return $blob; |
|
} |
|
|
|
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string |
|
{ |
|
$pathData = []; |
|
|
|
foreach ($ops as $op) { |
|
switch (true) { |
|
case $op instanceof Move: |
|
$fromX = $toX = round($op->getX(), self::PRECISION); |
|
$fromY = $toY = round($op->getY(), self::PRECISION); |
|
$pathData[] = sprintf('%s %s m', $toX, $toY); |
|
break; |
|
|
|
case $op instanceof Line: |
|
$fromX = $toX = round($op->getX(), self::PRECISION); |
|
$fromY = $toY = round($op->getY(), self::PRECISION); |
|
$pathData[] = sprintf('%s %s l', $toX, $toY); |
|
break; |
|
|
|
case $op instanceof EllipticArc: |
|
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY); |
|
break; |
|
|
|
case $op instanceof Curve: |
|
$x1 = round($op->getX1(), self::PRECISION); |
|
$y1 = round($op->getY1(), self::PRECISION); |
|
$x2 = round($op->getX2(), self::PRECISION); |
|
$y2 = round($op->getY2(), self::PRECISION); |
|
$fromX = $x3 = round($op->getX3(), self::PRECISION); |
|
$fromY = $y3 = round($op->getY3(), self::PRECISION); |
|
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3); |
|
break; |
|
|
|
case $op instanceof Close: |
|
$pathData[] = 'z'; |
|
break; |
|
|
|
default: |
|
throw new RuntimeException('Unexpected draw operation: ' . get_class($op)); |
|
} |
|
} |
|
|
|
return implode(' ', $pathData); |
|
} |
|
|
|
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void |
|
{ |
|
$startColor = $gradient->getStartColor(); |
|
$endColor = $gradient->getEndColor(); |
|
|
|
if ($startColor instanceof Alpha) { |
|
$startColor = $startColor->getBaseColor(); |
|
} |
|
|
|
$startColorType = get_class($startColor); |
|
|
|
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) { |
|
$startColorType = Cmyk::class; |
|
$startColor = $startColor->toCmyk(); |
|
} |
|
|
|
if (get_class($endColor) !== $startColorType) { |
|
switch ($startColorType) { |
|
case Cmyk::class: |
|
$endColor = $endColor->toCmyk(); |
|
break; |
|
|
|
case Rgb::class: |
|
$endColor = $endColor->toRgb(); |
|
break; |
|
|
|
case Gray::class: |
|
$endColor = $endColor->toGray(); |
|
break; |
|
} |
|
} |
|
|
|
$this->eps .= "eoclip\n<<\n"; |
|
|
|
if ($gradient->getType() === GradientType::RADIAL()) { |
|
$this->eps .= " /ShadingType 3\n"; |
|
} else { |
|
$this->eps .= " /ShadingType 2\n"; |
|
} |
|
|
|
$this->eps .= " /Extend [ true true ]\n" |
|
. " /AntiAlias true\n"; |
|
|
|
switch ($startColorType) { |
|
case Cmyk::class: |
|
$this->eps .= " /ColorSpace /DeviceCMYK\n"; |
|
break; |
|
|
|
case Rgb::class: |
|
$this->eps .= " /ColorSpace /DeviceRGB\n"; |
|
break; |
|
|
|
case Gray::class: |
|
$this->eps .= " /ColorSpace /DeviceGray\n"; |
|
break; |
|
} |
|
|
|
switch ($gradient->getType()) { |
|
case GradientType::HORIZONTAL(): |
|
$this->eps .= sprintf( |
|
" /Coords [ %s %s %s %s ]\n", |
|
round($x, self::PRECISION), |
|
round($y, self::PRECISION), |
|
round($x + $width, self::PRECISION), |
|
round($y, self::PRECISION) |
|
); |
|
break; |
|
|
|
case GradientType::VERTICAL(): |
|
$this->eps .= sprintf( |
|
" /Coords [ %s %s %s %s ]\n", |
|
round($x, self::PRECISION), |
|
round($y, self::PRECISION), |
|
round($x, self::PRECISION), |
|
round($y + $height, self::PRECISION) |
|
); |
|
break; |
|
|
|
case GradientType::DIAGONAL(): |
|
$this->eps .= sprintf( |
|
" /Coords [ %s %s %s %s ]\n", |
|
round($x, self::PRECISION), |
|
round($y, self::PRECISION), |
|
round($x + $width, self::PRECISION), |
|
round($y + $height, self::PRECISION) |
|
); |
|
break; |
|
|
|
case GradientType::INVERSE_DIAGONAL(): |
|
$this->eps .= sprintf( |
|
" /Coords [ %s %s %s %s ]\n", |
|
round($x, self::PRECISION), |
|
round($y + $height, self::PRECISION), |
|
round($x + $width, self::PRECISION), |
|
round($y, self::PRECISION) |
|
); |
|
break; |
|
|
|
case GradientType::RADIAL(): |
|
$centerX = ($x + $width) / 2; |
|
$centerY = ($y + $height) / 2; |
|
|
|
$this->eps .= sprintf( |
|
" /Coords [ %s %s 0 %s %s %s ]\n", |
|
round($centerX, self::PRECISION), |
|
round($centerY, self::PRECISION), |
|
round($centerX, self::PRECISION), |
|
round($centerY, self::PRECISION), |
|
round(max($width, $height) / 2, self::PRECISION) |
|
); |
|
break; |
|
} |
|
|
|
$this->eps .= " /Function\n" |
|
. " <<\n" |
|
. " /FunctionType 2\n" |
|
. " /Domain [ 0 1 ]\n" |
|
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor)) |
|
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor)) |
|
. " /N 1\n" |
|
. " >>\n>>\nshfill\nQ\n"; |
|
} |
|
|
|
private function getColorSetString(ColorInterface $color) : string |
|
{ |
|
if ($color instanceof Rgb) { |
|
return $this->getColorString($color) . ' rgb'; |
|
} |
|
|
|
if ($color instanceof Cmyk) { |
|
return $this->getColorString($color) . ' cmyk'; |
|
} |
|
|
|
if ($color instanceof Gray) { |
|
return $this->getColorString($color) . ' gray'; |
|
} |
|
|
|
return $this->getColorSetString($color->toCmyk()); |
|
} |
|
|
|
private function getColorString(ColorInterface $color) : string |
|
{ |
|
if ($color instanceof Rgb) { |
|
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255); |
|
} |
|
|
|
if ($color instanceof Cmyk) { |
|
return sprintf( |
|
'%s %s %s %s', |
|
$color->getCyan() / 100, |
|
$color->getMagenta() / 100, |
|
$color->getYellow() / 100, |
|
$color->getBlack() / 100 |
|
); |
|
} |
|
|
|
if ($color instanceof Gray) { |
|
return sprintf('%s', $color->getGray() / 100); |
|
} |
|
|
|
return $this->getColorString($color->toCmyk()); |
|
} |
|
}
|
|
|