started work on *magick driver and updated tests
14
.github/workflows/phpstan.yaml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: PHPStan
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
phpstan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: php-actions/composer@v6
|
||||||
|
with:
|
||||||
|
dev: yes
|
||||||
|
- uses: php-actions/phpstan@v3
|
||||||
|
with:
|
||||||
|
path: src
|
||||||
|
level: max
|
13
.github/workflows/phpunit.yaml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
name: PHPUnit
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
phpunit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: php-actions/composer@v6
|
||||||
|
with:
|
||||||
|
dev: yes
|
||||||
|
- uses: php-actions/phpunit@v3
|
||||||
|
with:
|
||||||
|
version: 10
|
3
.gitignore
vendored
|
@ -3,4 +3,5 @@ composer.phar
|
||||||
composer.lock
|
composer.lock
|
||||||
debug.log
|
debug.log
|
||||||
/examples/out/*
|
/examples/out/*
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
.phpunit.result.cache
|
16
README.md
|
@ -8,12 +8,11 @@ This library is under active development, and until a 1.0 release is made you sh
|
||||||
|
|
||||||
### Current progress
|
### Current progress
|
||||||
|
|
||||||
| Driver | Rotate | Mirror | Resize | Crop |
|
| Driver | Rotate | Mirror | Resize | Crop |
|
||||||
| :--------- | :----: | :----: | :----: | :--: |
|
| :-------------- | :----: | :----: | :----: | :---: |
|
||||||
| GD | X | X | X | X |
|
| GDDriver | X | X | X | X |
|
||||||
| Imagick | | | | |
|
| MagickDriver | | | | |
|
||||||
| Gmagick | | | | |
|
| MagickCliDriver | X | X | X | X |
|
||||||
| ImagickCLI | X | X | X | X |
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
@ -22,9 +21,8 @@ This library is under active development, and until a 1.0 release is made you sh
|
||||||
A 1.0 release will not be made until the following drivers are available and solidly tested:
|
A 1.0 release will not be made until the following drivers are available and solidly tested:
|
||||||
|
|
||||||
* GD
|
* GD
|
||||||
* Imagick
|
* Magick (unified driver that will use either ImageMagick or Gmagick automatically depending on what is available)
|
||||||
* Gmagick
|
* MagickCliDriver (unified driver that can be configured to use either ImageMagick or Gmagick CLI tools)
|
||||||
* GmagickCLI
|
|
||||||
|
|
||||||
### Transforms
|
### Transforms
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,13 @@
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"ext-gmagick": "*",
|
"ext-gmagick": "*",
|
||||||
"ext-gd": "*"
|
"ext-gd": "*",
|
||||||
|
"phpunit/phpunit": "^10.3",
|
||||||
|
"phpstan/phpstan": "^1.10"
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"ByJoby\\ImageTransform\\tests\\": "tests/"
|
"ByJoby\\ImageTransform\\tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "./vendor/bin/atoum -d tests"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
phpstan.neon
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
parameters:
|
||||||
|
level: max
|
||||||
|
paths:
|
||||||
|
- src
|
31
phpunit.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
executionOrder="random"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnEmptyTestSuite="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
verbose="true"
|
||||||
|
bootstrap="vendor/autoload.php">
|
||||||
|
<php>
|
||||||
|
<ini name="display_errors" value="On" />
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
<ini name="xdebug.mode" value="coverage" />
|
||||||
|
</php>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Tests">
|
||||||
|
<directory>tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<coverage>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
<report>
|
||||||
|
<html outputDirectory="coverage" lowUpperBound="50" highLowerBound="90" />
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
|
@ -10,7 +10,8 @@ use ByJoby\ImageTransform\Drivers\GDDriver;
|
||||||
*/
|
*/
|
||||||
class DefaultDriver
|
class DefaultDriver
|
||||||
{
|
{
|
||||||
protected $driver;
|
/** @var DriverInterface|null */
|
||||||
|
protected static $driver;
|
||||||
|
|
||||||
public static function get(): DriverInterface
|
public static function get(): DriverInterface
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,10 +4,4 @@ namespace ByJoby\ImageTransform\Drivers;
|
||||||
|
|
||||||
abstract class AbstractCliDriver extends AbstractDriver
|
abstract class AbstractCliDriver extends AbstractDriver
|
||||||
{
|
{
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
if (!function_exists('exec')) {
|
|
||||||
throw new \Exception("CLI drivers can't be used with the current configuration because exec is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,25 @@ namespace ByJoby\ImageTransform\Drivers;
|
||||||
use ByJoby\ImageTransform\DriverInterface;
|
use ByJoby\ImageTransform\DriverInterface;
|
||||||
use ByJoby\ImageTransform\Image;
|
use ByJoby\ImageTransform\Image;
|
||||||
use ByJoby\ImageTransform\Sizers\AbstractSizer;
|
use ByJoby\ImageTransform\Sizers\AbstractSizer;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
abstract class AbstractDriver implements DriverInterface
|
abstract class AbstractDriver implements DriverInterface
|
||||||
{
|
{
|
||||||
|
/** @var string|null */
|
||||||
protected $tempDir = null;
|
protected $tempDir = null;
|
||||||
|
/** @var int */
|
||||||
protected $chmod_dir = 0775;
|
protected $chmod_dir = 0775;
|
||||||
|
/** @var int */
|
||||||
protected $chmod_file = 0665;
|
protected $chmod_file = 0665;
|
||||||
|
|
||||||
abstract protected function doSave(Image $image, string $filename);
|
abstract protected function doSave(Image $image, string $filename): void;
|
||||||
|
|
||||||
public function tempDir(): string
|
public function tempDir(): string
|
||||||
{
|
{
|
||||||
if (!$this->tempDir) {
|
if (!$this->tempDir) {
|
||||||
$this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("", true));
|
$this->setTempDir(sys_get_temp_dir() . '/byjoby_image-transform/' . uniqid("", true));
|
||||||
}
|
}
|
||||||
|
// @phpstan-ignore-next-line this is actually checked
|
||||||
return $this->tempDir;
|
return $this->tempDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +36,7 @@ abstract class AbstractDriver implements DriverInterface
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mkdir(string $dir)
|
protected function mkdir(string $dir): bool
|
||||||
{
|
{
|
||||||
// return true if dir exists and is writeable
|
// return true if dir exists and is writeable
|
||||||
if (is_dir($dir) && is_writeable($dir)) {
|
if (is_dir($dir) && is_writeable($dir)) {
|
||||||
|
@ -74,12 +79,15 @@ abstract class AbstractDriver implements DriverInterface
|
||||||
}
|
}
|
||||||
touch($filename);
|
touch($filename);
|
||||||
}
|
}
|
||||||
$this->doSave($image, realpath($filename));
|
$filename = realpath($filename);
|
||||||
|
if (!$filename) throw new Exception("Invalid filename or path");
|
||||||
|
$this->doSave($image, $filename);
|
||||||
chmod($filename, $this->chmod_file);
|
chmod($filename, $this->chmod_file);
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
$filename = $this->tempDir() . '/' . uniqid() . '.jpg';
|
$filename = $this->tempDir() . '/' . uniqid() . '.jpg';
|
||||||
$this->doSave($image, $filename);
|
$this->doSave($image, $filename);
|
||||||
|
/** @var string we can count on this being a string because we just wrote it */
|
||||||
$output = file_get_contents($filename);
|
$output = file_get_contents($filename);
|
||||||
unlink($filename);
|
unlink($filename);
|
||||||
return $output;
|
return $output;
|
||||||
|
|
|
@ -6,14 +6,20 @@ use ByJoby\ImageTransform\Image;
|
||||||
|
|
||||||
abstract class AbstractExtensionDriver extends AbstractDriver
|
abstract class AbstractExtensionDriver extends AbstractDriver
|
||||||
{
|
{
|
||||||
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
abstract protected function getImageObject(Image $image);
|
abstract protected function getImageObject(Image $image);
|
||||||
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
abstract protected function doResize($object, Image $image);
|
abstract protected function doResize($object, Image $image);
|
||||||
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
abstract protected function doCrop($object, Image $image);
|
abstract protected function doCrop($object, Image $image);
|
||||||
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
abstract protected function doFlip($object, Image $image);
|
abstract protected function doFlip($object, Image $image);
|
||||||
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
abstract protected function doRotation($object, Image $image);
|
abstract protected function doRotation($object, Image $image);
|
||||||
abstract protected function saveImageObject($object, string $filename);
|
// @phpstan-ignore-next-line we specify types in subclasses
|
||||||
|
abstract protected function saveImageObject($object, string $filename): void;
|
||||||
|
|
||||||
public function doSave(Image $image, string $filename)
|
public function doSave(Image $image, string $filename): void
|
||||||
{
|
{
|
||||||
$object = $this->getImageObject($image);
|
$object = $this->getImageObject($image);
|
||||||
$object = $this->doRotation($object, $image);
|
$object = $this->doRotation($object, $image);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace ByJoby\ImageTransform\Drivers;
|
namespace ByJoby\ImageTransform\Drivers;
|
||||||
|
|
||||||
use ByJoby\ImageTransform\Image;
|
use ByJoby\ImageTransform\Image;
|
||||||
|
use GdImage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This driver uses PHP's built-in GD libary. This is by far the slowest driver,
|
* This driver uses PHP's built-in GD libary. This is by far the slowest driver,
|
||||||
|
@ -18,42 +19,62 @@ class GDDriver extends AbstractExtensionDriver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getImageObject(Image $image)
|
protected function getImageObject(Image $image): GdImage
|
||||||
{
|
{
|
||||||
$source = $image->source();
|
$source = $image->source();
|
||||||
$extension = strtolower(preg_replace('/^.+\./', '', $source));
|
/** @var string */
|
||||||
|
$extension = preg_replace('/^.+\./', '', $source);
|
||||||
|
$extension = strtolower($extension);
|
||||||
switch ($extension) {
|
switch ($extension) {
|
||||||
case 'bmp':
|
case 'bmp':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefrombmp($source);
|
return imagecreatefrombmp($source);
|
||||||
case 'gif':
|
case 'gif':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromgif($source);
|
return imagecreatefromgif($source);
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromjpeg($source);
|
return imagecreatefromjpeg($source);
|
||||||
case 'jpeg':
|
case 'jpeg':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromjpeg($source);
|
return imagecreatefromjpeg($source);
|
||||||
case 'png':
|
case 'png':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefrompng($source);
|
return imagecreatefrompng($source);
|
||||||
case 'wbmp':
|
case 'wbmp':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromwbmp($source);
|
return imagecreatefromwbmp($source);
|
||||||
case 'webp':
|
case 'webp':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromwebp($source);
|
return imagecreatefromwebp($source);
|
||||||
case 'xbm':
|
case 'xbm':
|
||||||
|
// @phpstan-ignore-next-line this will throw an exception, which is good
|
||||||
return imagecreatefromxbm($source);
|
return imagecreatefromxbm($source);
|
||||||
default:
|
default:
|
||||||
throw new \Exception("Unsupported input type: " . htmlentities($source));
|
throw new \Exception("Unsupported input type: " . htmlentities($source));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doResize($object, Image $image)
|
/**
|
||||||
|
* @param GdImage $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return GdImage
|
||||||
|
*/
|
||||||
|
protected function doResize($object, Image $image): GdImage
|
||||||
{
|
{
|
||||||
$sizer = $image->sizer();
|
$sizer = $image->sizer();
|
||||||
if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) {
|
if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) {
|
||||||
// sizer is calling for a resize
|
// sizer is calling for a resize
|
||||||
|
/** @var GdImage */
|
||||||
$new = imagecreatetruecolor($sizer->resizeToWidth(), $sizer->resizeToHeight());
|
$new = imagecreatetruecolor($sizer->resizeToWidth(), $sizer->resizeToHeight());
|
||||||
imagecopyresampled(
|
imagecopyresampled(
|
||||||
$new, $object,
|
$new,
|
||||||
0, 0,//dst x/y
|
$object,
|
||||||
0, 0,
|
0,
|
||||||
|
0,
|
||||||
|
//dst x/y
|
||||||
|
0,
|
||||||
|
0,
|
||||||
$sizer->resizeToWidth(), $sizer->resizeToHeight(),
|
$sizer->resizeToWidth(), $sizer->resizeToHeight(),
|
||||||
$sizer->originalWidth(), $sizer->originalHeight()
|
$sizer->originalWidth(), $sizer->originalHeight()
|
||||||
);
|
);
|
||||||
|
@ -64,17 +85,26 @@ class GDDriver extends AbstractExtensionDriver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doCrop($object, Image $image)
|
/**
|
||||||
|
* @param GdImage $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return GdImage
|
||||||
|
*/
|
||||||
|
protected function doCrop($object, Image $image): GdImage
|
||||||
{
|
{
|
||||||
$sizer = $image->sizer();
|
$sizer = $image->sizer();
|
||||||
if ($sizer->cropToHeight() && $sizer->cropToWidth()) {
|
if ($sizer->cropToHeight() && $sizer->cropToWidth()) {
|
||||||
// sizer is calling for a crop
|
// sizer is calling for a crop
|
||||||
|
/** @var GdImage */
|
||||||
$new = imagecreatetruecolor($sizer->cropToWidth(), $sizer->cropToHeight());
|
$new = imagecreatetruecolor($sizer->cropToWidth(), $sizer->cropToHeight());
|
||||||
imagecopyresampled(
|
imagecopyresampled(
|
||||||
$new, $object,
|
$new,
|
||||||
($sizer->cropToWidth()-$sizer->resizeToWidth())/2,($sizer->cropToHeight()-$sizer->resizeToHeight())/2,
|
$object,
|
||||||
0,0,
|
($sizer->cropToWidth() - $sizer->resizeToWidth()) / 2, ($sizer->cropToHeight() - $sizer->resizeToHeight()) / 2,
|
||||||
$sizer->resizetoWidth(),$sizer->resizeToHeight(),$sizer->resizeToWidth(),$sizer->resizeToHeight()
|
0,
|
||||||
|
0,
|
||||||
|
// @phpstan-ignore-next-line these are definitely set
|
||||||
|
$sizer->resizetoWidth(), $sizer->resizeToHeight(), $sizer->resizeToWidth(), $sizer->resizeToHeight()
|
||||||
);
|
);
|
||||||
return $new;
|
return $new;
|
||||||
} else {
|
} else {
|
||||||
|
@ -83,47 +113,65 @@ class GDDriver extends AbstractExtensionDriver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doFlip($object, Image $image)
|
/**
|
||||||
|
* @param GdImage $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return GdImage
|
||||||
|
*/
|
||||||
|
protected function doFlip($object, Image $image): GdImage
|
||||||
{
|
{
|
||||||
if ($image->getFlipH()) {
|
if ($image->getFlipH()) {
|
||||||
imageflip($object,IMG_FLIP_HORIZONTAL);
|
imageflip($object, IMG_FLIP_HORIZONTAL);
|
||||||
}
|
}
|
||||||
if ($image->getFlipV()) {
|
if ($image->getFlipV()) {
|
||||||
imageflip($object,IMG_FLIP_VERTICAL);
|
imageflip($object, IMG_FLIP_VERTICAL);
|
||||||
}
|
}
|
||||||
return $object;
|
return $object;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doRotation($object, Image $image)
|
/**
|
||||||
|
* @param GdImage $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return GdImage
|
||||||
|
*/
|
||||||
|
protected function doRotation($object, Image $image): GdImage
|
||||||
{
|
{
|
||||||
if ($rotationAmount = 360 - $image->rotation() * 90) {
|
if ($rotationAmount = 360 - $image->rotation() * 90) {
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
return imagerotate($object, $rotationAmount, 0);
|
return imagerotate($object, $rotationAmount, 0);
|
||||||
}
|
}
|
||||||
return $object;
|
return $object;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function saveImageObject($object, string $filename)
|
/**
|
||||||
|
* @param GdImage $object
|
||||||
|
* @param string $filename
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function saveImageObject($object, string $filename): void
|
||||||
{
|
{
|
||||||
$extension = strtolower(preg_replace('/^.+\./', '', $filename));
|
/** @var string */
|
||||||
|
$extension = preg_replace('/^.+\./', '', $filename);
|
||||||
|
$extension = strtolower($extension);
|
||||||
switch ($extension) {
|
switch ($extension) {
|
||||||
case 'bmp':
|
case 'bmp':
|
||||||
return imagebmp($object, $filename);
|
imagebmp($object, $filename);
|
||||||
case 'gif':
|
case 'gif':
|
||||||
return imagegif($object, $filename);
|
imagegif($object, $filename);
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
return imagejpeg($object, $filename);
|
imagejpeg($object, $filename);
|
||||||
case 'jpeg':
|
case 'jpeg':
|
||||||
return imagejpeg($object, $filename);
|
imagejpeg($object, $filename);
|
||||||
case 'png':
|
case 'png':
|
||||||
return imagepng($object, $filename);
|
imagepng($object, $filename);
|
||||||
case 'wbmp':
|
case 'wbmp':
|
||||||
return imagewbmp($object, $filename);
|
imagewbmp($object, $filename);
|
||||||
case 'webp':
|
case 'webp':
|
||||||
return imagewebp($object, $filename);
|
imagewebp($object, $filename);
|
||||||
case 'xbm':
|
case 'xbm':
|
||||||
return imagexbm($object, $filename);
|
imagexbm($object, $filename);
|
||||||
default:
|
default:
|
||||||
throw new \Exception("Unsupported output type: " . htmlentities($filename));
|
throw new \Exception("Unsupported output type: " . htmlentities($filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,12 +13,8 @@ use ByJoby\ImageTransform\Image;
|
||||||
*/
|
*/
|
||||||
class MagickCliDriver extends AbstractCliDriver
|
class MagickCliDriver extends AbstractCliDriver
|
||||||
{
|
{
|
||||||
protected $mogrify_executable;
|
public function __construct(protected string $mogrify_executable = 'magick mogrify')
|
||||||
|
|
||||||
public function __construct($mogrify_executable = 'magick mogrify')
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
|
||||||
$this->mogrify_executable = $mogrify_executable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mogrifyExecutable(): string
|
protected function mogrifyExecutable(): string
|
||||||
|
@ -26,7 +22,7 @@ class MagickCliDriver extends AbstractCliDriver
|
||||||
return $this->mogrify_executable;
|
return $this->mogrify_executable;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doSave(Image $image, string $filename)
|
protected function doSave(Image $image, string $filename): void
|
||||||
{
|
{
|
||||||
// basics of command
|
// basics of command
|
||||||
$command = [
|
$command = [
|
||||||
|
|
144
src/Drivers/MagickDriver.php
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
|
||||||
|
namespace ByJoby\ImageTransform\Drivers;
|
||||||
|
|
||||||
|
use ByJoby\ImageTransform\Image;
|
||||||
|
use Gmagick;
|
||||||
|
use GmagickPixel;
|
||||||
|
use Imagick;
|
||||||
|
use ImagickPixel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This driver uses either ImageMagick or Gmagick, depending on its constructor
|
||||||
|
* argument.
|
||||||
|
*/
|
||||||
|
class MagickDriver extends AbstractExtensionDriver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* By default this driver will use Imagick, but if you pass a true value it
|
||||||
|
* will instead use Gmagick.
|
||||||
|
*
|
||||||
|
* @param boolean $use_gmagick
|
||||||
|
*/
|
||||||
|
public function __construct(protected bool $use_gmagick = false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getImageObject(Image $image): Gmagick|Imagick
|
||||||
|
{
|
||||||
|
if ($this->use_gmagick) return new Gmagick($image->source());
|
||||||
|
else return new Imagick($image->source());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Gmagick|Imagick $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return Gmagick|Imagick
|
||||||
|
*/
|
||||||
|
protected function doResize($object, Image $image): Gmagick|Imagick
|
||||||
|
{
|
||||||
|
$sizer = $image->sizer();
|
||||||
|
if ($sizer->resizeToHeight() && $sizer->resizeToWidth()) {
|
||||||
|
if ($object instanceof Gmagick) {
|
||||||
|
$object->resizeimage(
|
||||||
|
$sizer->resizeToWidth(),
|
||||||
|
$sizer->resizeToHeight(),
|
||||||
|
Gmagick::FILTER_LANCZOS,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$object->resizeImage(
|
||||||
|
$sizer->resizeToWidth(),
|
||||||
|
$sizer->resizeToHeight(),
|
||||||
|
Imagick::FILTER_LANCZOS,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Gmagick|Imagick $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return Gmagick|Imagick
|
||||||
|
*/
|
||||||
|
protected function doCrop($object, Image $image): Gmagick|Imagick
|
||||||
|
{
|
||||||
|
$sizer = $image->sizer();
|
||||||
|
$height = $sizer->cropToHeight();
|
||||||
|
$width = $sizer->cropToWidth();
|
||||||
|
if ($height && $width) {
|
||||||
|
$x = intval(($sizer->resizetoWidth() - $width) / 2);
|
||||||
|
$y = intval(($sizer->resizetoHeight() - $height) / 2);
|
||||||
|
if ($object instanceof Gmagick) {
|
||||||
|
$object->cropimage(
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
$x,
|
||||||
|
$y
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$object->cropImage(
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
$x,
|
||||||
|
$y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Gmagick|Imagick $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return Gmagick|Imagick
|
||||||
|
*/
|
||||||
|
protected function doFlip($object, Image $image): Gmagick|Imagick
|
||||||
|
{
|
||||||
|
if ($image->getFlipH()) {
|
||||||
|
if ($object instanceof Gmagick) $object->flipimage();
|
||||||
|
else $object->flipImage();
|
||||||
|
}
|
||||||
|
if ($image->getFlipV()) {
|
||||||
|
if ($object instanceof Gmagick) $object->flopimage();
|
||||||
|
else $object->flopImage();
|
||||||
|
}
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Gmagick|Imagick $object
|
||||||
|
* @param Image $image
|
||||||
|
* @return Gmagick|Imagick
|
||||||
|
*/
|
||||||
|
protected function doRotation($object, Image $image): Gmagick|Imagick
|
||||||
|
{
|
||||||
|
if ($image->rotation()) {
|
||||||
|
if ($object instanceof Gmagick) $object->rotateimage(new GmagickPixel("#000"), $image->rotation() * 90);
|
||||||
|
else $object->rotateImage(new ImagickPixel("#000"), $image->rotation() * 90);
|
||||||
|
}
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Gmagick|Imagick $object
|
||||||
|
* @param string $filename
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function saveImageObject($object, string $filename): void
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
$format = preg_replace('/^.+\./', '', $filename);
|
||||||
|
$format = strtoupper($format);
|
||||||
|
if ($format == 'JPG') $format = 'JPEG';
|
||||||
|
if ($object instanceof Gmagick) {
|
||||||
|
$object->setimageformat($format);
|
||||||
|
$object->writeimage($filename, true);
|
||||||
|
} else {
|
||||||
|
$object->setImageFormat($format);
|
||||||
|
$object->writeImage($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,21 @@ use ByJoby\ImageTransform\Sizers\Original;
|
||||||
|
|
||||||
class Image
|
class Image
|
||||||
{
|
{
|
||||||
|
/** @var string */
|
||||||
protected $source;
|
protected $source;
|
||||||
|
/** @var DriverInterface|null */
|
||||||
protected $driver;
|
protected $driver;
|
||||||
|
/** @var int */
|
||||||
protected $originalWidth;
|
protected $originalWidth;
|
||||||
|
/** @var int */
|
||||||
protected $originalHeight;
|
protected $originalHeight;
|
||||||
|
/** @var int */
|
||||||
protected $rotation = 0;
|
protected $rotation = 0;
|
||||||
|
/** @var boolean */
|
||||||
protected $flipH = false;
|
protected $flipH = false;
|
||||||
|
/** @var boolean */
|
||||||
protected $flipV = false;
|
protected $flipV = false;
|
||||||
|
/** @var AbstractSizer */
|
||||||
protected $sizer = null;
|
protected $sizer = null;
|
||||||
|
|
||||||
public function __construct(string $source, AbstractSizer|null $sizer = null)
|
public function __construct(string $source, AbstractSizer|null $sizer = null)
|
||||||
|
@ -30,10 +38,11 @@ class Image
|
||||||
public function setSource(string $source): static
|
public function setSource(string $source): static
|
||||||
{
|
{
|
||||||
// set source
|
// set source
|
||||||
$this->source = realpath($source);
|
$source = realpath($source);
|
||||||
if (!$this->source) {
|
if (!$source) {
|
||||||
throw new \Exception("Source image not found: " . htmlentities($source));
|
throw new \Exception("Source image not found");
|
||||||
}
|
}
|
||||||
|
$this->source = $source;
|
||||||
// validate file
|
// validate file
|
||||||
if (!is_file($this->source)) {
|
if (!is_file($this->source)) {
|
||||||
throw new \Exception("Image file doesn't exist: " . htmlentities($this->source));
|
throw new \Exception("Image file doesn't exist: " . htmlentities($this->source));
|
||||||
|
@ -42,7 +51,11 @@ class Image
|
||||||
throw new \Exception("Invalid image file: " . htmlentities($this->source));
|
throw new \Exception("Invalid image file: " . htmlentities($this->source));
|
||||||
}
|
}
|
||||||
// get height/width
|
// get height/width
|
||||||
list($this->originalWidth, $this->originalHeight) = getimagesize($this->source);
|
$size = getimagesize($this->source);
|
||||||
|
if (!$size) {
|
||||||
|
throw new \Exception("Couldn't get image size: " . htmlentities($this->source));
|
||||||
|
}
|
||||||
|
list($this->originalWidth, $this->originalHeight) = $size;
|
||||||
// return self
|
// return self
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +68,7 @@ class Image
|
||||||
public function setSizer(AbstractSizer $sizer): static
|
public function setSizer(AbstractSizer $sizer): static
|
||||||
{
|
{
|
||||||
$this->sizer = clone $sizer;
|
$this->sizer = clone $sizer;
|
||||||
$this->sizer->image($this);
|
$this->sizer->setImage($this);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use ByJoby\ImageTransform\Image;
|
||||||
|
|
||||||
abstract class AbstractSizer
|
abstract class AbstractSizer
|
||||||
{
|
{
|
||||||
|
/** @var Image */
|
||||||
protected $image;
|
protected $image;
|
||||||
|
|
||||||
abstract public function width(): int;
|
abstract public function width(): int;
|
||||||
|
@ -46,8 +47,9 @@ abstract class AbstractSizer
|
||||||
return $this->originalWidth()/$this->originalHeight();
|
return $this->originalWidth()/$this->originalHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function image(Image $image)
|
public function setImage(Image $image): static
|
||||||
{
|
{
|
||||||
$this->image = $image;
|
$this->image = $image;
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace ByJoby\ImageTransform\Sizers;
|
||||||
|
|
||||||
class Cover extends AbstractSizer
|
class Cover extends AbstractSizer
|
||||||
{
|
{
|
||||||
|
/** @var int */
|
||||||
protected $width, $height;
|
protected $width, $height;
|
||||||
|
|
||||||
public function __construct(int $width, int $height)
|
public function __construct(int $width, int $height)
|
||||||
|
@ -17,14 +18,17 @@ class Cover extends AbstractSizer
|
||||||
return $this->width / $this->height;
|
return $this->width / $this->height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{height:int,width:int}
|
||||||
|
*/
|
||||||
protected function calculateSize(): array
|
protected function calculateSize(): array
|
||||||
{
|
{
|
||||||
if ($this->targetRatio() < $this->originalRatio()) {
|
if ($this->targetRatio() < $this->originalRatio()) {
|
||||||
$height = $this->height;
|
$height = $this->height;
|
||||||
$width = round($height * $this->originalRatio());
|
$width = intval(round($height * $this->originalRatio()));
|
||||||
} else {
|
} else {
|
||||||
$width = $this->width;
|
$width = $this->width;
|
||||||
$height = round($width / $this->originalRatio());
|
$height = intval(round($width / $this->originalRatio()));
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
'height' => $height,
|
'height' => $height,
|
||||||
|
@ -61,4 +65,4 @@ class Cover extends AbstractSizer
|
||||||
{
|
{
|
||||||
return $this->height;
|
return $this->height;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ namespace ByJoby\ImageTransform\Sizers;
|
||||||
|
|
||||||
class Fit extends AbstractSizer
|
class Fit extends AbstractSizer
|
||||||
{
|
{
|
||||||
|
/** @var int */
|
||||||
protected $width, $height;
|
protected $width, $height;
|
||||||
|
|
||||||
public function __construct(int $width, int $height)
|
public function __construct(int $width, int $height)
|
||||||
|
@ -27,14 +28,17 @@ class Fit extends AbstractSizer
|
||||||
return $this->width / $this->height;
|
return $this->width / $this->height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{height:int,width:int}
|
||||||
|
*/
|
||||||
protected function calculateSize(): array
|
protected function calculateSize(): array
|
||||||
{
|
{
|
||||||
if ($this->targetRatio() > $this->originalRatio()) {
|
if ($this->targetRatio() > $this->originalRatio()) {
|
||||||
$height = $this->height;
|
$height = $this->height;
|
||||||
$width = round($height * $this->originalRatio());
|
$width = intval(round($height * $this->originalRatio()));
|
||||||
} else {
|
} else {
|
||||||
$width = $this->width;
|
$width = $this->width;
|
||||||
$height = round($width / $this->originalRatio());
|
$height = intval(round($width / $this->originalRatio()));
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
'height' => $height,
|
'height' => $height,
|
||||||
|
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 5.6 KiB |
109
tests/Sizers/CoverTest.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
/* image-transform | https://github.com/jobyone/image-transform | MIT License */
|
||||||
|
namespace ByJoby\ImageTransform\Sizers;
|
||||||
|
|
||||||
|
use ByJoby\ImageTransform\Image;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CoverTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSquareImageSizedSquare(): void
|
||||||
|
{
|
||||||
|
// mock 200x200 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(200);
|
||||||
|
// spin up a 50x50 sizer
|
||||||
|
$sizer = new Cover(50, 50);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(50, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(50, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSquareImageSizedLandscape(): void
|
||||||
|
{
|
||||||
|
// mock 200x200 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(200);
|
||||||
|
// spin up a 100x50 sizer
|
||||||
|
$sizer = new Cover(100, 50);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(100, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(100, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(100, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
public function testSquareImageSizedPortrait(): void
|
||||||
|
{
|
||||||
|
// mock 200x200 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(200);
|
||||||
|
// spin up a 50x100 sizer
|
||||||
|
$sizer = new Cover(50, 100);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(100, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(100, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(50, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(100, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLandscapeImageSizedSquare(): void
|
||||||
|
{
|
||||||
|
// mock 200x100 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(100);
|
||||||
|
// spin up a 50x50 sizer
|
||||||
|
$sizer = new Cover(50, 50);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(100, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(50, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLandscapeImageSizedLandscape(): void
|
||||||
|
{
|
||||||
|
// mock 200x100 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(100);
|
||||||
|
// spin up a 100x50 sizer
|
||||||
|
$sizer = new Cover(100, 50);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(100, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(100, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(50, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
public function testLandscapeImageSizedPortrait(): void
|
||||||
|
{
|
||||||
|
// mock 200x100 image
|
||||||
|
$image = $this->createMock(Image::class);
|
||||||
|
$image->method("originalWidth")->willReturn(200);
|
||||||
|
$image->method("originalHeight")->willReturn(100);
|
||||||
|
// spin up a 50x100 sizer
|
||||||
|
$sizer = new Cover(50, 100);
|
||||||
|
$sizer->setImage($image);
|
||||||
|
// resized to cover
|
||||||
|
$this->assertEquals(200, $sizer->resizeToWidth());
|
||||||
|
$this->assertEquals(100, $sizer->resizeToHeight());
|
||||||
|
// cropped to size
|
||||||
|
$this->assertEquals(50, $sizer->cropToWidth());
|
||||||
|
$this->assertEquals(100, $sizer->cropToHeight());
|
||||||
|
}
|
||||||
|
}
|
BIN
tests/input-100x200.png
Normal file
After Width: | Height: | Size: 774 B |
BIN
tests/input-200x100.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
tests/input-200x200.png
Normal file
After Width: | Height: | Size: 834 B |
196
tests/test.svg
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="200"
|
||||||
|
height="200"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
sodipodi:docname="test.svg"
|
||||||
|
inkscape:export-filename="input-200x200.png.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="2.3267609"
|
||||||
|
inkscape:cx="94.552044"
|
||||||
|
inkscape:cy="68.980014"
|
||||||
|
inkscape:window-width="1736"
|
||||||
|
inkscape:window-height="1023"
|
||||||
|
inkscape:window-x="156"
|
||||||
|
inkscape:window-y="153"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g3">
|
||||||
|
<rect
|
||||||
|
style="fill:#0000ff;stroke-width:0.188982"
|
||||||
|
id="rect1"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="0"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0.188982"
|
||||||
|
id="rect1-6"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="28.571428"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ff00ff;stroke-width:0.188982"
|
||||||
|
id="rect1-61"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="57.142857"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0.188982"
|
||||||
|
id="rect1-6-8"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="85.714287"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#00ffff;stroke-width:0.188982"
|
||||||
|
id="rect1-61-0"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="114.28571"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0.188982"
|
||||||
|
id="rect1-6-8-2"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="142.85715"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000080;stroke-width:0.447214"
|
||||||
|
id="rect2"
|
||||||
|
width="40"
|
||||||
|
height="50"
|
||||||
|
x="0"
|
||||||
|
y="150" />
|
||||||
|
<rect
|
||||||
|
style="fill:#f2f2f2;stroke-width:0.632456"
|
||||||
|
id="rect3"
|
||||||
|
width="40"
|
||||||
|
height="50"
|
||||||
|
x="40"
|
||||||
|
y="150" />
|
||||||
|
<rect
|
||||||
|
style="fill:#cccccc;stroke-width:0.188982"
|
||||||
|
id="rect1-61-0-9"
|
||||||
|
width="28.571428"
|
||||||
|
height="25"
|
||||||
|
x="171.42857"
|
||||||
|
y="-150"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#cccccc;stroke-width:0.422577"
|
||||||
|
id="rect1-1"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="0"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#808000;stroke-width:0.422577"
|
||||||
|
id="rect1-6-2"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="28.571428"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#008080;stroke-width:0.422577"
|
||||||
|
id="rect1-61-9"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="57.142857"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#008000;stroke-width:0.422577"
|
||||||
|
id="rect1-6-8-3"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="85.714287"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#800080;stroke-width:0.422577"
|
||||||
|
id="rect1-61-0-1"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="114.28571"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#800000;stroke-width:0.422577"
|
||||||
|
id="rect1-6-8-2-9"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="142.85715"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000080;stroke-width:0.422577"
|
||||||
|
id="rect1-61-0-9-4"
|
||||||
|
width="28.571428"
|
||||||
|
height="125"
|
||||||
|
x="171.42857"
|
||||||
|
y="-125"
|
||||||
|
transform="scale(1,-1)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#800080;stroke-width:0.632456"
|
||||||
|
id="rect3-5"
|
||||||
|
width="40"
|
||||||
|
height="50"
|
||||||
|
x="80"
|
||||||
|
y="150" />
|
||||||
|
<rect
|
||||||
|
style="fill:#4d4d4d;stroke-width:0.632456"
|
||||||
|
id="rect3-5-2"
|
||||||
|
width="40"
|
||||||
|
height="50"
|
||||||
|
x="120"
|
||||||
|
y="150" />
|
||||||
|
<rect
|
||||||
|
style="fill:#1a1a1a;stroke-width:0.632456"
|
||||||
|
id="rect3-5-2-7"
|
||||||
|
width="40"
|
||||||
|
height="50"
|
||||||
|
x="160"
|
||||||
|
y="150" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.1 KiB |