diff --git a/README.md b/README.md index 1931cba..246d8cc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The stitcher needs two things: declare(strict_types=1); +use Intervention\Image\Drivers\Gd\Driver; +use Intervention\Image\ImageManager; use WyriHaximus\TileStitcher\Coordinate; use WyriHaximus\TileStitcher\Dimensions; use WyriHaximus\TileStitcher\Stitcher; @@ -41,7 +43,13 @@ $tiles = [ ), ]; -Stitcher::stitch( +$stitcher = new Stitcher( + new ImageManager( + new Driver(), + ), +); + +$stitcher->stitch( Map::calculate( new Dimensions(512, 512), ...$tiles, @@ -58,9 +66,9 @@ The result: - [X] `Map::calculateMap` method to calculate the size of the resulting map image - [X] `Switcher::stitch` method to take the `Map` and stitch it together into an image -- [ ] Pick up desired image format from render output argument, it's PNG only now +- [X] Pick up desired image format from render output argument, it's PNG only now - [ ] Support pointing at directory and pick up all images utilizing a callable to parse coordinates -- [ ] Switch to abstraction layer for image operations +- [X] Switch to abstraction layer for image operations - [ ] Dynamic tile sizes + scaling up any tiles smaller than the largest tile # License diff --git a/composer.json b/composer.json index 786b8f3..9a15f09 100644 --- a/composer.json +++ b/composer.json @@ -10,10 +10,11 @@ ], "require": { "php": "^8.2", - "ext-gd": "^8.2", + "intervention/image": "^3.6", "thecodingmachine/safe": "^2.5" }, "require-dev": { + "ext-gd": "^8.2", "wyrihaximus/test-utilities": "^6.0.7" }, "autoload": { diff --git a/composer.lock b/composer.lock index c2510f0..9e2e95a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,144 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5eb38f89cbaadc6845e1a2f90c529e1b", + "content-hash": "39fcf9653218bf1841833a002e0e278f", "packages": [ + { + "name": "intervention/gif", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.1.0" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-03-26T17:23:47+00:00" + }, + { + "name": "intervention/image", + "version": "3.6.4", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "193324ec88bc5ad4039e57ce9b926ae28dfde813" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/193324ec88bc5ad4039e57ce9b926ae28dfde813", + "reference": "193324ec88bc5ad4039e57ce9b926ae28dfde813", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.1", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.6.4" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-05-08T13:53:15+00:00" + }, { "name": "thecodingmachine/safe", "version": "v2.5.0", @@ -1097,16 +1233,16 @@ }, { "name": "composer/pcre", - "version": "3.1.3", + "version": "3.1.4", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "reference": "04229f163664973f68f38f6f73d917799168ef24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", + "reference": "04229f163664973f68f38f6f73d917799168ef24", "shasum": "" }, "require": { @@ -1148,7 +1284,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.1.4" }, "funding": [ { @@ -1164,7 +1300,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-05-27T13:40:54+00:00" }, { "name": "composer/semver", @@ -3229,12 +3365,12 @@ "version": "v5.2.13", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", + "url": "https://github.com/jsonrainbow/json-schema.git", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", "shasum": "" }, @@ -3289,8 +3425,8 @@ "schema" ], "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13" }, "time": "2023-09-26T02:20:38+00:00" }, @@ -3510,16 +3646,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.11", + "version": "1.6.12", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "81a161d0b135df89951abd52296adf97deb0723d" + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/81a161d0b135df89951abd52296adf97deb0723d", - "reference": "81a161d0b135df89951abd52296adf97deb0723d", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { @@ -3589,7 +3725,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2024-03-21T18:34:15+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { "name": "myclabs/deep-copy", @@ -5157,16 +5293,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.4.0", + "version": "5.4.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "298d2febfe79d03fe714eb871d5538da55205b1a" + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a", - "reference": "298d2febfe79d03fe714eb871d5538da55205b1a", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", "shasum": "" }, "require": { @@ -5215,9 +5351,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" }, - "time": "2024-04-09T21:13:58+00:00" + "time": "2024-05-21T05:55:05+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -5326,16 +5462,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.0", + "version": "1.11.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "666cb1703742cea9cc80fee631f0940e1592fa6e" + "reference": "0d5d4294a70deb7547db655c47685d680e39cfec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/666cb1703742cea9cc80fee631f0940e1592fa6e", - "reference": "666cb1703742cea9cc80fee631f0940e1592fa6e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec", + "reference": "0d5d4294a70deb7547db655c47685d680e39cfec", "shasum": "" }, "require": { @@ -5380,7 +5516,7 @@ "type": "github" } ], - "time": "2024-05-13T06:02:22+00:00" + "time": "2024-05-24T13:23:04+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -6609,16 +6745,16 @@ }, { "name": "react/promise", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { @@ -6670,7 +6806,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.1.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -6678,7 +6814,7 @@ "type": "open_collective" } ], - "time": "2023-11-16T16:21:57+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "revolt/event-loop", @@ -8407,16 +8543,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.2", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", - "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", "shasum": "" }, "require": { @@ -8483,7 +8619,7 @@ "type": "open_collective" } ], - "time": "2024-04-23T20:25:34+00:00" + "time": "2024-05-22T21:24:41+00:00" }, { "name": "symfony/config", @@ -10841,25 +10977,25 @@ }, { "name": "wyrihaximus/phpstan-rules-wrapper", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/WyriHaximus/php-phpstan-rules-wrapper.git", - "reference": "b8b4dcf99d96494b42feb5afe1cdea8b5b5b6fbe" + "reference": "e781d15c0316a8c72599ec50a18fce97aae12c73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WyriHaximus/php-phpstan-rules-wrapper/zipball/b8b4dcf99d96494b42feb5afe1cdea8b5b5b6fbe", - "reference": "b8b4dcf99d96494b42feb5afe1cdea8b5b5b6fbe", + "url": "https://api.github.com/repos/WyriHaximus/php-phpstan-rules-wrapper/zipball/e781d15c0316a8c72599ec50a18fce97aae12c73", + "reference": "e781d15c0316a8c72599ec50a18fce97aae12c73", "shasum": "" }, "require": { "ergebnis/phpstan-rules": "^2.2.0", "php": "^8.2", - "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-deprecation-rules": "^1.2.0", "phpstan/phpstan-mockery": "^1.1.2", - "phpstan/phpstan-phpunit": "^1.3.16", - "phpstan/phpstan-strict-rules": "^1.5.5", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", "thecodingmachine/phpstan-safe-rule": "^1.2", "thecodingmachine/phpstan-strict-rules": "^1.0" }, @@ -10877,7 +11013,7 @@ "description": "🌯 PHPStan rules wrapper", "support": { "issues": "https://github.com/WyriHaximus/php-phpstan-rules-wrapper/issues", - "source": "https://github.com/WyriHaximus/php-phpstan-rules-wrapper/tree/6.0.1" + "source": "https://github.com/WyriHaximus/php-phpstan-rules-wrapper/tree/6.1.0" }, "funding": [ { @@ -10885,7 +11021,7 @@ "type": "github" } ], - "time": "2024-04-20T21:08:05+00:00" + "time": "2024-05-15T05:39:12+00:00" }, { "name": "wyrihaximus/test-utilities", @@ -10968,10 +11104,11 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.2", + "php": "^8.2" + }, + "platform-dev": { "ext-gd": "^8.2" }, - "platform-dev": [], "platform-overrides": { "php": "8.2.13" }, diff --git a/etc/qa/phpstan.neon b/etc/qa/phpstan.neon index 6284e7a..229f58f 100644 --- a/etc/qa/phpstan.neon +++ b/etc/qa/phpstan.neon @@ -1,6 +1,5 @@ parameters: ignoreErrors: - '#GdImage#' - - '#imagefill#' includes: - ../../vendor/wyrihaximus/test-utilities/rules.neon diff --git a/src/Stitcher.php b/src/Stitcher.php index 2101b9b..a9a54a6 100644 --- a/src/Stitcher.php +++ b/src/Stitcher.php @@ -4,42 +4,31 @@ namespace WyriHaximus\TileStitcher; -use function imagecolorallocatealpha; -use function Safe\file_get_contents; -use function Safe\imagecopy; -use function Safe\imagecreate; -use function Safe\imagecreatefromstring; -use function Safe\imagefill; -use function Safe\imagepng; +use Intervention\Image\ImageManager; -use const PNG_NO_FILTER; - -final class Stitcher +final readonly class Stitcher { - private const SRC_XY = 0; - private const RGB_TRANSPARENT = [0, 0, 0, 127]; - private const OFFSET = 1; + public function __construct(private ImageManager $imageManager) + { + } + + private const PLACEMENT_POSITION = 'top-left'; + private const IMAGE_OUTPUT_QUALITY = 0; + private const OFFSET = 1; - public static function stitch(Map $map, string $output): void + public function stitch(Map $map, string $output): void { - $image = imagecreate($map->dimensions->width, $map->dimensions->height); - /** @psalm-suppress InvalidArgument */ - imagefill($image, self::SRC_XY, self::SRC_XY, imagecolorallocatealpha($image, ...self::RGB_TRANSPARENT)); + $image = $this->imageManager->create($map->dimensions->width, $map->dimensions->height); foreach ($map->tiles as $tile) { - $tileImage = imagecreatefromstring(file_get_contents($tile->fileName)); - imagecopy( - $image, - $tileImage, + $image->place( + $this->imageManager->read($tile->fileName), + self::PLACEMENT_POSITION, (($tile->coordinate->x + self::OFFSET) * $map->tileSize->width) - (($map->lowest->x + self::OFFSET) * $map->tileSize->width), (($tile->coordinate->y + self::OFFSET) * $map->tileSize->height) - (($map->lowest->y + self::OFFSET) * $map->tileSize->height), - self::SRC_XY, - self::SRC_XY, - $map->tileSize->width, - $map->tileSize->height, ); } - imagepng($image, $output, 0, PNG_NO_FILTER); + $image->save($output, quality: self::IMAGE_OUTPUT_QUALITY); } } diff --git a/tests/StitcherTest.php b/tests/StitcherTest.php index 8802ef7..1a1cb40 100644 --- a/tests/StitcherTest.php +++ b/tests/StitcherTest.php @@ -4,6 +4,8 @@ namespace WyriHaximus\Tests\TileStitcher; +use Intervention\Image\Drivers\Gd\Driver; +use Intervention\Image\ImageManager; use WyriHaximus\TestUtilities\TestCase; use WyriHaximus\TileStitcher\Dimensions; use WyriHaximus\TileStitcher\Map; @@ -28,7 +30,7 @@ final class StitcherTest extends TestCase public function render(int $expectedWidth, int $expectedHeight, string $expectedOutput, Tile ...$tiles): void { $output = $this->getTmpDir() . time() . '-' . md5($expectedOutput) . '.png'; - Stitcher::stitch( + (new Stitcher(new ImageManager(new Driver())))->stitch( Map::calculate( new Dimensions(512, 512), ...$tiles, @@ -53,10 +55,10 @@ private function compareImages(int $expectedWidth, int $expectedHeight, string $ $rgbResult = imagecolorat($imResult, $x, $y); $colorsResult = imagecolorsforindex($imResult, $rgbResult); + self::assertEquals($colorsExpectedResult['alpha'], $colorsResult['alpha']); self::assertEquals($colorsExpectedResult['red'], $colorsResult['red']); self::assertEquals($colorsExpectedResult['green'], $colorsResult['green']); self::assertEquals($colorsExpectedResult['blue'], $colorsResult['blue']); - self::assertEquals($colorsExpectedResult['alpha'], $colorsResult['alpha']); } } diff --git a/tests/maps/isles.png b/tests/maps/isles.png index 1ffbbc5..e1ea825 100644 Binary files a/tests/maps/isles.png and b/tests/maps/isles.png differ