diff --git a/.travis.yml b/.travis.yml index fa5217c..f5834e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 - nightly - hhvm-nightly diff --git a/README.md b/README.md index f9a8693..e581915 100644 --- a/README.md +++ b/README.md @@ -9,60 +9,62 @@ processing extensions (Gd, Imagick) are not required. This allows developers to eliminate some portability issues from their applications. -## Requirements +### Features + +- Format support includes PNG, GIF, BMP and the Netpbm formats (See docs: [File formats](https://gfx-php.readthedocs.io/en/latest/user/formats.html)). +- Support for scaling, cropping, format conversion and colorspace transformations (See docs: [Image operations](https://gfx-php.readthedocs.io/en/latest/user/operations.html)). +- Pure PHP: This library does not require Gd, ImageMagick or GraphicsMagick extensions. + +## Quick start + +### Requirements - PHP 7.0 or newer. -- zlib extension, for reading PNG files. +- `zlib` extension, for reading PNG files. -## Get started +### Installation -- Have a read of the documentation at [gfx-php.readthedocs.io](https://gfx-php.readthedocs.io/) -- See the `examples/` sub-folder for snippets. +Install `gfx-php` with composer: -## Status & scope +```bash +composer install mike42/gfx-php +``` -Currently, we are implementing basic raster operations on select file formats. +### Basic usage -See related documentation for: +The basic usage is like this: -- [Available input file formats](https://gfx-php.readthedocs.io/en/latest/user/formats.html#input-formats). -- [Available output file formats](https://gfx-php.readthedocs.io/en/latest/user/formats.html#output-formats). -- [Available image operations](https://gfx-php.readthedocs.io/en/latest/user/operations.html). +```php + write("test.gif"); +``` -If you're interested in image processing algorithms, then please consider contributing an implementation. +### Further reading -For algorithms, it appears feasable to implement: +- Read of the documentation at [gfx-php.readthedocs.io](https://gfx-php.readthedocs.io/) +- See the `examples/` sub-folder for snippets. -- Rotate -- Layered operations -- Affine transformations -- Lines, arcs, circles, and rectangles. +## Contributing -And sill on the roadmap for format support: +This project is open to all kinds of contributions, including suggestions, documentation fixes, examples, formats and image processing algorithms. -- BMP input, which involves RLE decompression (BMP output is already available). -- GIF input, which involves LZW decompression (GIF output is already available). -- TIFF input and output, which also involves LZW (de)compression. +Some ideas for improvement listed in [the issue tracker](https://travis-ci.org/mike42/gfx-php). Code contributions must be releasable under the LGPLv3 or later. -In the interests of getting the basic features working first, there is no current plan to attempt lossy compression, or formats that are not common on either the web or for printing, eg: +### Scope -- JPEG -- MNG -- PAM format -- XPM -- .. etc. +As a small project, we can't do everything. In particular, `gfx-php` is not likely to ever perform non-raster operations: -Also, as we don't have the luxury of pulling in dependencies, I'm considering anything that is not a raster operation out-of-scope: +- vector image formats (PDF, SVG, EPS, etc). +- anything involving vector fonts -- All vector image formats (PDF, SVG, EPS, etc). -- Anything involving vector fonts +### Acknowledgements -### Test data sets +This repository uses test files from other projects: -- [imagetestsuite](https://code.google.com/archive/p/imagetestsuite/) -- [bmpsuite](http://entropymine.com/jason/bmpsuite/) -- [pngsuite](http://www.schaik.com/pngsuite/) -- [jburkardt's data sets](https://people.sc.fsu.edu/~jburkardt/data/) +- [PyGIF](https://github.com/robert-ancell/pygif) test suite by Robert Ancell. +- [pngsuite](http://www.schaik.com/pngsuite/) by Willem van Schaik. ## Similar projects diff --git a/composer.json b/composer.json index 67130e8..96f6c76 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,9 @@ "php": "7.0.0" } }, - "require": {}, + "require": { + "php": ">=7.0.0" + }, "autoload": { "psr-4": { "Mike42\\": "src/Mike42" @@ -23,6 +25,6 @@ }, "require-dev": { "phpunit/phpunit": "^6.5", - "squizlabs/php_codesniffer": "^3.2" + "squizlabs/php_codesniffer": "^3.3.1" } } diff --git a/composer.lock b/composer.lock index a197b5f..a92393b 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f18fe674495aa912f567c0918655e1c0", + "content-hash": "454d7cbc4dd9812fdf60367796418349", "packages": [], "packages-dev": [ { @@ -362,33 +362,33 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.5", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -421,20 +421,20 @@ "spy", "stub" ], - "time": "2018-02-19T10:16:54+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "5.3.0", + "version": "5.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" + "reference": "c89677919c5dd6d3b3852f230a663118762218ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", - "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac", "shasum": "" }, "require": { @@ -484,7 +484,7 @@ "testing", "xunit" ], - "time": "2017-12-06T09:29:45+00:00" + "time": "2018-04-06T15:36:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -674,16 +674,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.7", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -701,7 +701,7 @@ "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", + "phpunit/phpunit-mock-objects": "^5.0.9", "sebastian/comparator": "^2.1", "sebastian/diff": "^2.0", "sebastian/environment": "^3.1", @@ -754,20 +754,20 @@ "testing", "xunit" ], - "time": "2018-02-26T07:01:09+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.6", + "version": "5.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", "shasum": "" }, "require": { @@ -780,7 +780,7 @@ "phpunit/phpunit": "<6.0" }, "require-dev": { - "phpunit/phpunit": "^6.5" + "phpunit/phpunit": "^6.5.11" }, "suggest": { "ext-soap": "*" @@ -813,7 +813,7 @@ "mock", "xunit" ], - "time": "2018-01-06T05:45:45+00:00" + "time": "2018-08-09T05:50:03+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1376,16 +1376,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.2.3", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "4842476c434e375f9d3182ff7b89059583aa8b27" + "reference": "379deb987e26c7cd103a7b387aea178baec96e48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4842476c434e375f9d3182ff7b89059583aa8b27", - "reference": "4842476c434e375f9d3182ff7b89059583aa8b27", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48", + "reference": "379deb987e26c7cd103a7b387aea178baec96e48", "shasum": "" }, "require": { @@ -1423,7 +1423,65 @@ "phpcs", "standards" ], - "time": "2018-02-20T21:35:23+00:00" + "time": "2018-12-19T23:57:18+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" }, { "name": "theseer/tokenizer", @@ -1467,20 +1525,21 @@ }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -1513,7 +1572,7 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], @@ -1521,7 +1580,9 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "php": ">=7.0.0" + }, "platform-dev": [], "platform-overrides": { "php": "7.0.0" diff --git a/docs/user/formats.rst b/docs/user/formats.rst index b731db9..405c4c1 100644 --- a/docs/user/formats.rst +++ b/docs/user/formats.rst @@ -40,6 +40,16 @@ All valid PNG files can be read, including: This library currently has limited support for transparency, and will discard any alpha channel from a PNG file when it is loaded. +GIF +^^^ + +The GIF codec is used where the input has the ``gif`` file extension. Any well-formed GIF file can be read, but there are some limitations: + +- If a GIF file contains multiple images, then only the first one will be loaded +- If a transparent color is present, then this will be mixed to white + +A GIF image will always be loaded into an omstamce of :class:`IndexedRasterImage`, which makes palette information available. + Netpbm Formats ^^^^^^^^^^^^^^ diff --git a/src/Mike42/GfxPhp/Codec/Common/DataBlobInputStream.php b/src/Mike42/GfxPhp/Codec/Common/DataBlobInputStream.php index 119c061..6b5993b 100644 --- a/src/Mike42/GfxPhp/Codec/Common/DataBlobInputStream.php +++ b/src/Mike42/GfxPhp/Codec/Common/DataBlobInputStream.php @@ -30,7 +30,7 @@ public function peek(int $bytes) } $read = strlen($chunk); if ($read !== $bytes) { - throw new \Exception("Unexpected end of file, needed $read but read $bytes"); + throw new \Exception("Unexpected end of file, needed $bytes but read $read"); } return $chunk; } diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php new file mode 100644 index 0000000..cf861ea --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php @@ -0,0 +1,53 @@ +appIdentifer = $appIdentifer; + $this->appAuthCode = $appAuthCode; + $this->data = $data; + } + + public function getAppIdentifer(): string + { + return $this->appIdentifer; + } + + public function getAppAuthCode(): string + { + return $this->appAuthCode; + } + + public function getData(): array + { + return $this->data; + } + + public static function fromBin(DataInputStream $in) : GifApplicationExt + { + $extIntroducer = $in->read(1); + $extLabel = $in->read(1); + if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_APPLICATION) { + throw new \Exception("Not a GIF application extension block"); + } + $lenData = $in->read(1); + $len = unpack("C", $lenData)[1]; + if ($len != 11) { + throw new \Exception("Incorrect size on application extension block"); + } + $appIdentifier = $in->read(8); + $appAuthCode = $in->read(3); + $data = GifData::readDataSubBlocks($in); + return new GifApplicationExt($appIdentifier, $appAuthCode, $data); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifColorTable.php b/src/Mike42/GfxPhp/Codec/Gif/GifColorTable.php new file mode 100644 index 0000000..54c2d85 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifColorTable.php @@ -0,0 +1,26 @@ + palette = $palette; + } + + public static function fromBin(\Mike42\GfxPhp\Codec\Common\DataInputStream $in, int $globalColorTableSize) + { + $tableData = $in -> read($globalColorTableSize * 3); + $paletteArr = array_values(unpack("C*", $tableData)); + $palette = array_chunk($paletteArr, 3); + return new GifColorTable($palette); + } + + public function getPalette(): array + { + return $this->palette; + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php new file mode 100644 index 0000000..8c6a64e --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php @@ -0,0 +1,32 @@ +data; + } + + public function __construct(array $data) + { + $this -> data = $data; + } + + public static function fromBin(DataInputStream $in) + { + $extIntroducer = $in->read(1); + $extLabel = $in->read(1); + if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_COMMENT) { + throw new \Exception("Not a GIF comment extension block"); + } + $data = GifData::readDataSubBlocks($in); + return new GifCommentExt($data); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifData.php b/src/Mike42/GfxPhp/Codec/Gif/GifData.php new file mode 100644 index 0000000..238e46b --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifData.php @@ -0,0 +1,80 @@ +graphicsBlock = $graphicsBlock; + $this->specialPurposeBlock = $specialPurposeBlock; + $this->unrecognisedBlock = $unrecognisedBlock; + } + + public function getUnrecognisedBlock() + { + return $this->unrecognisedBlock; + } + + public function getSpecialPurposeBlock() + { + return $this->specialPurposeBlock; + } + + public function getGraphicsBlock() + { + return $this->graphicsBlock; + } + + public static function fromBin(DataInputStream $in) : GifData + { + $peek = $in -> peek(2); + $blockId = $peek[0]; + $extensionId = $peek[1]; + if ($blockId == GifData::GIF_EXTENSION) { + // Special-purpose blocks + if ($extensionId == GifData::GIF_EXTENSION_APPLICATION) { + $applicationExt = GifApplicationExt::fromBin($in); + $specialPurposeBlock = new GifSpecialPurposeBlock($applicationExt, null); + return new GifData(null, $specialPurposeBlock, null); + } else if ($extensionId == GifData::GIF_EXTENSION_COMMENT) { + $commentExt = GifCommentExt::fromBin($in); + $specialPurposeBlock = new GifSpecialPurposeBlock(null, $commentExt); + return new GifData(null, $specialPurposeBlock, null); + } + } + // Unknown extension blocks + if ($blockId == GifData::GIF_EXTENSION && $extensionId != GifData::GIF_EXTENSION_GRAPHIC_CONTROL && $extensionId != GifData::GIF_EXTENSION_PLAINTEXT) { + $unrecognisedBlock = GifUnknownExt::fromBin($in); + return new GifData(null, null, $unrecognisedBlock); + } + $graphicsBlock = GifGraphicsBlock::fromBin($in); + return new GifData($graphicsBlock, null, null); + } + + public static function readDataSubBlocks(DataInputStream $in) : array + { + $blocks = []; + while ($in -> peek(1) != "\x00") { + $blockSizeData = $in -> read(1); + $blockSize = unpack("C", $blockSizeData)[1]; + $blocks[] = $in -> read($blockSize); + } + $in -> read(1); // Discard terminating byte + return $blocks; + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php new file mode 100644 index 0000000..fb26a61 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -0,0 +1,123 @@ + header = $header; + $this -> logicalScreen = $logicalScreen; + $this -> data = $data; + $this -> trailer = $trailer; + } + + public static function fromBinary(DataInputStream $data) : GifDataStream + { + // Check header + $header = $data -> read(6); + if ($header != GifDataStream::GIF87_SIGNATURE && $header != GifDataStream::GIF89_SIGNATURE) { + throw new \Exception("Bad GIF header"); + } + $logicalScreen = GifLogicalScreen::fromBin($data); + $imageData = []; + while ($data -> peek(1) != GifDataStream::GIF_TRAILER) { + $imageData[] = GifData::fromBin($data); + } + $trailer = $data -> read(1); // Discard trailer byte + if (!$data -> isEof()) { + throw new \Exception("The GIF file is corrupt; data found after the GIF trailer"); + } + return new GifDataStream($header, $logicalScreen, $imageData, $trailer); + } + + public function toRasterImage(int $imageIndex = 0) : IndexedRasterImage + { + // Extract an image from the GIF + $currentIndex = 0; + foreach ($this -> data as $dataBlock) { + if ($dataBlock -> getGraphicsBlock() !== null && $dataBlock -> getGraphicsBlock() -> getTableBasedImage() != null) { + // This is a raster image + if ($currentIndex == $imageIndex) { + return GifDataStream::extractImage($this -> logicalScreen, $dataBlock -> getGraphicsBlock() -> getTableBasedImage(), $dataBlock -> getGraphicsBlock() -> getGraphicControlExt()); + } + $currentIndex++; + } + } + throw new \Exception("Could not find image #$imageIndex in GIF file"); + } + + private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBasedImage $tableBasedImage, GifGraphicControlExt $graphicControlExt = null) : IndexedRasterImage + { + + $width = $tableBasedImage->getImageDescriptor()->getWidth(); + $height = $tableBasedImage->getImageDescriptor()->getHeight(); + $colorTable = $tableBasedImage->getLocalColorTable() == null ? $logicalScreen->getGlobalColorTable() : $tableBasedImage->getLocalColorTable(); + if ($colorTable == null) { + throw new \Exception("GIF contains no color table for the image. Loading this type of file is not supported."); + } + if ($width == 0 || $height == 0) { + throw new \Exception("GIF contains no pixels. Loading this type of file is not supported."); + } + // De-compress the actual image data + $compressedData = join($tableBasedImage->getDataSubBlocks()); + $decompressedData = LzwCompression::decompress($compressedData, $tableBasedImage->getLzqMinSize()); + // Discard extra bytes here, since IndexedRasterImage will reject it + $actualLen = strlen($decompressedData); + $expectedLen = $width * $height; + if ($actualLen > $expectedLen) { + $decompressedData = substr($decompressedData, 0, $expectedLen); + } else if ($actualLen < $expectedLen) { + throw new \Exception("GIF corrupt or truncated. Expexted $expectedLen pixels for $width x $height image, but only $actualLen pixels were encoded."); + } + if ($tableBasedImage -> getImageDescriptor() -> isInterlaced()) { + $decompressedData = self::deinterlace($width, $decompressedData); + } + // Array of ints for IndexedRasterImage + $dataArr = array_values(unpack("C*", $decompressedData)); + $image = IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); + // Lastly, transparency handling + if ($graphicControlExt != null && $graphicControlExt -> hasTransparentColor()) { + $image -> setTransparentColor($graphicControlExt -> getTransparentColorIndex()); + } + return $image; + } + + private static function deinterlace(int $width, string $data) : string + { + // Four-pass GIF de-interlace. Reads input in order. + $old = str_split($data, $width); + $height = count($old); + $new = array_fill(0, $height, ""); + $j = 0; + for ($i = 0; $i < $height; $i += 8) { + $new[$i] = $old[$j]; + $j++; + } + for ($i = 4; $i < $height; $i += 8) { + $new[$i] = $old[$j]; + $j++; + } + for ($i = 2; $i < $height; $i += 4) { + $new[$i] = $old[$j]; + $j++; + } + for ($i = 1; $i < $height; $i += 2) { + $new[$i] = $old[$j]; + $j++; + } + return join($new); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php new file mode 100644 index 0000000..322e735 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php @@ -0,0 +1,77 @@ +disposalMethod = $disposalMethod; + $this->hasUserInputFlag = $hasUserInputFlag; + $this->hasTransparentColor = $hasTransparentColor; + $this->delayTime = $delayTime; + $this->transparentColorIndex = $transparentColorIndex; + } + + public function getDisposalMethod(): int + { + return $this->disposalMethod; + } + + public function hasUserInputFlag(): bool + { + return $this->hasUserInputFlag; + } + + public function hasTransparentColor(): bool + { + return $this->hasTransparentColor; + } + + public function getDelayTime(): int + { + return $this->delayTime; + } + + public function getTransparentColorIndex(): int + { + return $this->transparentColorIndex; + } + + public static function fromBin(DataInputStream $in) : GifGraphicControlExt + { + $extIntroducer = $in->read(1); + $extLabel = $in->read(1); + if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { + throw new \Exception("Not a GIF application extension block"); + } + $lenData = $in->read(1); + $len = unpack("C", $lenData)[1]; + if ($len != 4) { + throw new \Exception("Incorrect size on application extension block"); + } + $packedFieldData = $in -> read(1); + $packedFields = unpack("C", $packedFieldData)[1]; // Note 3 most-significant bits are reserved + $disposalMethod = ($packedFields >> 2) & 0x07; + $hasUserInputFlag = (($packedFields >> 1) & 0x01) == 1; + $hasTransparentColor = ($packedFields & 0x01) == 1; + $delayTimeData = $in -> read(2); + $delayTime = unpack("v", $delayTimeData)[1]; + $transparentColorIndexData = $in -> read(1); + $transparentColorIndex = unpack("C", $transparentColorIndexData)[1]; + $end = $in -> read(1); + if ($end != "\x00") { + throw new \Exception("GIF graphic control block not correctly terminated"); + } + return new GifGraphicControlExt($disposalMethod, $hasUserInputFlag, $hasTransparentColor, $delayTime, $transparentColorIndex); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php new file mode 100644 index 0000000..e2b3b5d --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -0,0 +1,70 @@ +graphicControlExt = $graphicControlExt; + $this->tableBasedImage = $tableBasedImage; + $this->plaintextExt = $plaintextExt; + } + + public function getGraphicControlExt() + { + return $this->graphicControlExt; + } + + public function getTableBasedImage() + { + return $this->tableBasedImage; + } + + public function getPlaintextExt() + { + return $this->plaintextExt; + } + + public static function fromBin(DataInputStream $in) : GifGraphicsBlock + { + $peek = $in -> peek(2); + $blockId = $peek[0]; + $extensionId = $peek[1]; + // Could have a graphic control extension before it + $graphicControlExtension = null; + if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { + // Optional graphic control extension + $graphicControlExtension = GifGraphicControlExt::fromBin($in); + // Re-populate for next block + $peek = $in -> peek(2); + $blockId = $peek[0]; + $extensionId = $peek[1]; + } + if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_APPLICATION) { + // ImageMagick drops an 'application' block here, which we can discard. + // We would need a slight re-structure to record this correctly. + $application = GifApplicationExt::fromBin($in); + $peek = $in -> peek(2); + $blockId = $peek[0]; + $extensionId = $peek[1]; + } + if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_PLAINTEXT) { + // Plain text + $plaintextExtension = GifPlaintextExt::fromBin($in); + return new GifGraphicsBlock($graphicControlExtension, null, $plaintextExtension); + } else if ($blockId == GifData::GIF_IMAGE_SEPARATOR) { + // Table-based image + $tableBasedImage = GifTableBasedImage::fromBin($in); + return new GifGraphicsBlock($graphicControlExtension, $tableBasedImage, null); + } + throw new \Exception("Could not recognise a graphics or extension block; GIF file is corrupt"); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifImageDescriptor.php b/src/Mike42/GfxPhp/Codec/Gif/GifImageDescriptor.php new file mode 100644 index 0000000..76f3e00 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifImageDescriptor.php @@ -0,0 +1,94 @@ +left = $left; + $this->top = $top; + $this->width = $width; + $this->height = $height; + $this->hasLocalColorTable = $hasLocalColorTable; + $this->isInterlaced = $isInterlaced; + $this->hasSortedLocalColorTable = $hasSortedLocalColorTable; + $this->localColorTableSize = $localColorTableSize; + } + + public function getLeft(): int + { + return $this->left; + } + + public function getTop(): int + { + return $this->top; + } + + public function getWidth(): int + { + return $this->width; + } + + public function getHeight(): int + { + return $this->height; + } + + public function hasLocalColorTable(): bool + { + return $this->hasLocalColorTable; + } + + public function isInterlaced(): bool + { + return $this->isInterlaced; + } + + public function hasSortedLocalColorTable(): bool + { + return $this->hasSortedLocalColorTable; + } + + public function getLocalColorTableSize(): int + { + return $this->localColorTableSize; + } + + public static function fromBin(DataInputStream $in): GifImageDescriptor + { + $imageSep = $in->read(1); + if ($imageSep != GifData::GIF_IMAGE_SEPARATOR) { + throw new \Exception("Not a GIF image descriptor block"); + } + $sizeData = $in -> read(8); + $size = unpack("v4", $sizeData); + $left = $size[1]; + $top = $size[2]; + $width = $size[3]; + $height = $size[4]; + $packedFieldData = $in->read(1); + $packedFields = unpack("C", $packedFieldData)[1]; + $hasLocalColorTable = ($packedFields >> 7) == 1; + $isInterlaced = (($packedFields >> 6) & 0x01) == 1; + $hasSortedLocalColorTable = (($packedFields >> 5) & 0x01) == 1; + // 2 bits are reserved here and not parsed + $localColorTableSize = $packedFields & 0x07; + return new GifImageDescriptor($left, $top, $width, $height, $hasLocalColorTable, $isInterlaced, $hasSortedLocalColorTable, $localColorTableSize); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreen.php b/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreen.php new file mode 100644 index 0000000..d0fd375 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreen.php @@ -0,0 +1,40 @@ +logicalScreenDescriptor = $logicalScreenDescriptor; + $this->globalColorTable = $globalColorTable; + } + + public function getLogicalScreenDescriptor(): GifLogicalScreenDescriptor + { + return $this->logicalScreenDescriptor; + } + + public function getGlobalColorTable(): GifColorTable + { + return $this->globalColorTable; + } + + public static function fromBin(DataInputStream $data): GifLogicalScreen + { + $logicalScreenDescriptor = GifLogicalScreenDescriptor::fromBin($data); + $globalColorTable = null; + if ($logicalScreenDescriptor->hasGlobalColorTable()) { + $globalColorTableSize = 1 << ($logicalScreenDescriptor->getGlobalColorTableSize() + 1); + $globalColorTable = GifColorTable::fromBin($data, $globalColorTableSize); + } + return new GifLogicalScreen($logicalScreenDescriptor, $globalColorTable); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreenDescriptor.php b/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreenDescriptor.php new file mode 100644 index 0000000..b7a770c --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreenDescriptor.php @@ -0,0 +1,90 @@ + width = $width; + $this -> height = $height; + $this -> hasGlobalColorTable = $hasGlobalColorTable; + $this -> colorResolution = $colorResolution; + $this -> hasSortedGlobalColorTable = $hasSortedGlobalColorTable; + $this -> globalColorTableSize = $globalColorTableSize; + $this -> backgroundColorIndex = $backgroundColorIndex; + $this -> pixelAspectRatio = $pixelAspectRatio; + } + + public static function fromBin(DataInputStream $in) : GifLogicalScreenDescriptor + { + $sizeData = $in -> read(4); + $size = unpack("v2", $sizeData); + $width = $size[1]; + $height = $size[2]; + $packedFieldData = $in -> read(1); + $packedFields = unpack("C", $packedFieldData)[1]; + $hasGlobalColorTable = ($packedFields >> 7) == 1; + $colorResolution = ($packedFields >> 4) & 0x0F; + $hasSortedGlobalColorTable = (($packedFields >> 3) & 0x01) == 1; + $globalColorTableSize = $packedFields & 0x07; + // Everything else + $otherFieldData = $in -> read(2); + $otherFields = unpack("C2", $otherFieldData); + $pixelAspectRatio = $otherFields[1]; + $backgroundColorIndex = $otherFields[2]; + return new GifLogicalScreenDescriptor($width, $height, $hasGlobalColorTable, $colorResolution, $hasSortedGlobalColorTable, $globalColorTableSize, $backgroundColorIndex, $pixelAspectRatio); + } + + public function getHeight(): int + { + return $this->height; + } + + public function hasGlobalColorTable(): bool + { + return $this->hasGlobalColorTable; + } + + public function getColorResolution(): int + { + return $this->colorResolution; + } + + public function hasSortedGlobalColorTabled(): bool + { + return $this->hasSortedGlobalColorTable; + } + + public function getGlobalColorTableSize(): int + { + return $this->globalColorTableSize; + } + + public function getBackgroundColorIndex(): int + { + return $this->backgroundColorIndex; + } + + public function getPixelAspectRatio(): int + { + return $this->pixelAspectRatio; + } + + public function getWidth(): int + { + return $this->width; + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php new file mode 100644 index 0000000..27c6d63 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php @@ -0,0 +1,47 @@ +header; + } + + public function getData(): array + { + return $this->data; + } + private $data; + + public function __construct(string $header, array $data) + { + $this -> header = $header; + $this -> data = $data; + } + + public static function fromBin(DataInputStream $in) : GifPlaintextExt + { + $introducer = $in->read(1); + $label = $in->read(1); + if ($introducer != GifData::GIF_EXTENSION || $label != GifData::GIF_EXTENSION_PLAINTEXT) { + throw new \Exception("Not a GIF plaintext block"); + } + $lenData = $in->read(1); + $len = unpack("C", $lenData)[1]; + if ($len != 12) { + throw new \Exception("Incorrect size on plain text block"); + } + // these 12 bytes have meaning, but we are more interested in correctly skipping past this info rather than parsing it, since the feature is quite obscure. + $header = $in -> read($len); + $data = GifData::readDataSubBlocks($in); + return new GifPlaintextExt($header, $data); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifSpecialPurposeBlock.php b/src/Mike42/GfxPhp/Codec/Gif/GifSpecialPurposeBlock.php new file mode 100644 index 0000000..45589c7 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifSpecialPurposeBlock.php @@ -0,0 +1,27 @@ +applicationExt = $applicationExt; + $this->commentExt = $commentExt; + } + + public function getApplicationExt() + { + return $this->applicationExt; + } + + public function getCommentExt() + { + return $this->commentExt; + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifTableBasedImage.php b/src/Mike42/GfxPhp/Codec/Gif/GifTableBasedImage.php new file mode 100644 index 0000000..161b7bf --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifTableBasedImage.php @@ -0,0 +1,57 @@ +imageDescriptor; + } + + public function getLzqMinSize(): int + { + return $this->lzqMinSize; + } + + public function getDataSubBlocks(): array + { + return $this->dataSubBlocks; + } + + public function getLocalColorTable() + { + return $this->localColorTable; + } + + public function __construct(GifImageDescriptor $imageDescriptor, int $lzqMinSize, array $dataSubBlocks, GifColorTable $localColorTable = null) + { + + $this->imageDescriptor = $imageDescriptor; + $this->lzqMinSize = $lzqMinSize; + $this->dataSubBlocks = $dataSubBlocks; + $this->localColorTable = $localColorTable; + } + + public static function fromBin(DataInputStream $in) : GifTableBasedImage + { + $imageDescriptor = GifImageDescriptor::fromBin($in); + $localColorTable = null; + if ($imageDescriptor->hasLocalColorTable()) { + $localColorTableSize = 1 << ($imageDescriptor->getLocalColorTableSize() + 1); + $localColorTable = GifColorTable::fromBin($in, $localColorTableSize); + } + $lzwMinSizeData = $in -> read(1); + $lzwMinSize = unpack("C", $lzwMinSizeData)[1]; + $dataSubBlocks = GifData::readDataSubBlocks($in); + return new GifTableBasedImage($imageDescriptor, $lzwMinSize, $dataSubBlocks, $localColorTable); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifUnknownExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifUnknownExt.php new file mode 100644 index 0000000..f9eeaec --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifUnknownExt.php @@ -0,0 +1,50 @@ +label = $label; + $this->header = $header; + $this->data = $data; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getHeader(): string + { + return $this->header; + } + + public function getData(): array + { + return $this->data; + } + + public static function fromBin(DataInputStream $in) + { + $introducer = $in->read(1); + if ($introducer != GifData::GIF_EXTENSION) { + throw new \Exception("Not a GIF extension block"); + } + $label = $in->read(1); + $lenData = $in->read(1); + $len = unpack("C", $lenData)[1]; + $header = $in -> read($len); + $data = GifData::readDataSubBlocks($in); + return new GifUnknownExt($label, $header, $data); + } +} diff --git a/src/Mike42/GfxPhp/Codec/GifCodec.php b/src/Mike42/GfxPhp/Codec/GifCodec.php index 7bc5413..26bf912 100644 --- a/src/Mike42/GfxPhp/Codec/GifCodec.php +++ b/src/Mike42/GfxPhp/Codec/GifCodec.php @@ -1,11 +1,13 @@ toRasterImage(); + } + public function getEncodeFormats(): array { return ["gif"]; diff --git a/src/Mike42/GfxPhp/Codec/ImageCodec.php b/src/Mike42/GfxPhp/Codec/ImageCodec.php index 77cd2ca..63b5fb9 100644 --- a/src/Mike42/GfxPhp/Codec/ImageCodec.php +++ b/src/Mike42/GfxPhp/Codec/ImageCodec.php @@ -59,6 +59,7 @@ public static function getInstance() : ImageCodec ]; $decoders = [ PngCodec::getInstance(), + GifCodec::getInstance(), PnmCodec::getInstance() ]; self::$instance = new ImageCodec($encoders, $decoders); diff --git a/src/Mike42/GfxPhp/IndexedRasterImage.php b/src/Mike42/GfxPhp/IndexedRasterImage.php index 585d9a7..43508e2 100644 --- a/src/Mike42/GfxPhp/IndexedRasterImage.php +++ b/src/Mike42/GfxPhp/IndexedRasterImage.php @@ -127,6 +127,10 @@ public function toIndexed(): IndexedRasterImage public function indexToRgb(int $index) { + if ($index == $this -> transparentColor) { + // White + return [255, 255, 255]; + } if ($index >= 0 && $index < count($this -> palette)) { // Defined index return $this -> palette[$index]; diff --git a/test/integration/GifsuiteTest.php b/test/integration/GifsuiteTest.php new file mode 100644 index 0000000..99a0469 --- /dev/null +++ b/test/integration/GifsuiteTest.php @@ -0,0 +1,495 @@ + loadImage("255-codes.gif"); + $this -> assertEquals(100, $img -> getWidth()); + $this -> assertEquals(100, $img -> getHeight()); + } + + function test_4095_codes_clear() { + $img = $this -> loadImage("4095-codes-clear.gif"); + $this -> assertEquals(100, $img -> getWidth()); + $this -> assertEquals(100, $img -> getHeight()); + } + + function test_4095_codes() { + $this -> markTestSkipped("Known bug: LZW overflow"); + $img = $this -> loadImage("4095-codes.gif"); + $this -> assertEquals(100, $img -> getWidth()); + $this -> assertEquals(100, $img -> getHeight()); + } + + function test_all_blues() { + $img = $this -> loadImage("all-blues.gif"); + $this -> assertEquals(16, $img -> getWidth()); + $this -> assertEquals(16, $img -> getHeight()); + } + + function test_all_greens() { + $img = $this -> loadImage("all-greens.gif"); + $this -> assertEquals(16, $img -> getWidth()); + $this -> assertEquals(16, $img -> getHeight()); + } + + function test_all_reds() { + $img = $this -> loadImage("all-reds.gif"); + $this -> assertEquals(16, $img -> getWidth()); + $this -> assertEquals(16, $img -> getHeight()); + } + + function test_animation() { + $img = $this -> loadImage("animation.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_animation_multi_image_explicit_zero_delay() { + $img = $this -> loadImage("animation-multi-image-explicit-zero-delay.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_animation_multi_image() { + $img = $this -> loadImage("animation-multi-image.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_animation_no_delays() { + $img = $this -> loadImage("animation-no-delays.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_animation_speed() { + $img = $this -> loadImage("animation-speed.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_animation_zero_delays() { + $img = $this -> loadImage("animation-zero-delays.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_comment() { + $img = $this -> loadImage("comment.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth1() { + $img = $this -> loadImage("depth1.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth2() { + $img = $this -> loadImage("depth2.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth3() { + $img = $this -> loadImage("depth3.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth4() { + $img = $this -> loadImage("depth4.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth5() { + $img = $this -> loadImage("depth5.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth6() { + $img = $this -> loadImage("depth6.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth7() { + $img = $this -> loadImage("depth7.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_depth8() { + $img = $this -> loadImage("depth8.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_disabled_transparent() { + $img = $this -> loadImage("disabled-transparent.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_dispose_keep() { + $img = $this -> loadImage("dispose-keep.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_dispose_none() { + $img = $this -> loadImage("dispose-none.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_dispose_restore_background() { + $img = $this -> loadImage("dispose-restore-background.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_dispose_restore_previous() { + $img = $this -> loadImage("dispose-restore-previous.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_double_clears() { + $img = $this -> loadImage("double-clears.gif"); + $this -> assertEquals(8, $img -> getWidth()); + $this -> assertEquals(8, $img -> getHeight()); + } + + function test_extra_data() { + $img = $this -> loadImage("extra-data.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_extra_pixels() { + $img = $this -> loadImage("extra-pixels.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_four_colors() { + $img = $this -> loadImage("four-colors.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_gif87a_animation() { + $img = $this -> loadImage("gif87a-animation.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_gif87a() { + $img = $this -> loadImage("gif87a.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_high_color() { + $img = $this -> loadImage("high-color.gif"); + $this -> assertEquals(16, $img -> getWidth()); + $this -> assertEquals(16, $img -> getHeight()); + } + + function test_icc_color_profile_empty() { + $img = $this -> loadImage("icc-color-profile-empty.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_icc_color_profile() { + $img = $this -> loadImage("icc-color-profile.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_image_inside_bg() { + $img = $this -> loadImage("image-inside-bg.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_image_outside_bg() { + $img = $this -> loadImage("image-outside-bg.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_image_overlap_bg() { + $img = $this -> loadImage("image-overlap-bg.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_images_combine() { + $img = $this -> loadImage("images-combine.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_images_overlap() { + $img = $this -> loadImage("images-overlap.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_image_zero_height() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("image-zero-height.gif"); + } + + function test_image_zero_size() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("image-zero-size.gif"); + } + + function test_image_zero_width() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("image-zero-width.gif"); + } + + function test_interlace() { + $img = $this -> loadImage("interlace.gif"); + $this -> assertEquals(16, $img -> getWidth()); + $this -> assertEquals(16, $img -> getHeight()); + } + + function test_invalid_ascii_comment() { + $img = $this -> loadImage("invalid-ascii-comment.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_invalid_background() { + $img = $this -> loadImage("invalid-background.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_invalid_code() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("invalid-code.gif"); + } + + function test_invalid_colors() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("invalid-colors.gif"); + } + + function test_invalid_transparent() { + $img = $this -> loadImage("invalid-transparent.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_invalid_utf8_comment() { + $img = $this -> loadImage("invalid-utf8-comment.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_large_codes() { + $img = $this -> loadImage("large-codes.gif"); + $this -> assertEquals(100, $img -> getWidth()); + $this -> assertEquals(100, $img -> getHeight()); + } + + function test_large_comment() { + $img = $this -> loadImage("large-comment.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_local_color_table() { + $img = $this -> loadImage("local-color-table.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_animexts() { + $img = $this -> loadImage("loop-animexts.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_buffer() { + $img = $this -> loadImage("loop-buffer.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_buffer_max() { + $img = $this -> loadImage("loop-buffer_max.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_infinite() { + $img = $this -> loadImage("loop-infinite.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_max() { + $img = $this -> loadImage("loop-max.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_loop_once() { + $img = $this -> loadImage("loop-once.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_many_clears() { + $img = $this -> loadImage("many-clears.gif"); + $this -> assertEquals(8, $img -> getWidth()); + $this -> assertEquals(8, $img -> getHeight()); + } + + function test_max_codes() { + $img = $this -> loadImage("max-codes.gif"); + $this -> assertEquals(100, $img -> getWidth()); + $this -> assertEquals(100, $img -> getHeight()); + } + + function test_max_height() { + $img = $this -> loadImage("max-height.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(65535, $img -> getHeight()); + } + + function test_max_size() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("max-size.gif"); + } + + function test_max_width() { + $img = $this -> loadImage("max-width.gif"); + $this -> assertEquals(65535, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_missing_pixels() { + $img = $this -> loadImage("missing-pixels.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_no_clear_and_eoi() { + $img = $this -> loadImage("no-clear-and-eoi.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_no_clear() { + $img = $this -> loadImage("no-clear.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_no_data() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("no-data.gif"); + } + + function test_no_eoi() { + $img = $this -> loadImage("no-eoi.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_no_global_color_table() { + $img = $this -> loadImage("no-global-color-table.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_nul_application_extension() { + $img = $this -> loadImage("nul-application-extension.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_nul_comment() { + $img = $this -> loadImage("nul-comment.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_plain_text() { + $img = $this -> loadImage("plain-text.gif"); + $this -> assertEquals(40, $img -> getWidth()); + $this -> assertEquals(8, $img -> getHeight()); + } + + function test_transparent() { + $img = $this -> loadImage("transparent.gif"); + $this -> assertEquals(2, $img -> getWidth()); + $this -> assertEquals(2, $img -> getHeight()); + } + + function test_unknown_application_extension() { + $img = $this -> loadImage("unknown-application-extension.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_unknown_extension() { + $img = $this -> loadImage("unknown-extension.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_xmp_data_empty() { + $img = $this -> loadImage("xmp-data-empty.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_xmp_data() { + $img = $this -> loadImage("xmp-data.gif"); + $this -> assertEquals(1, $img -> getWidth()); + $this -> assertEquals(1, $img -> getHeight()); + } + + function test_zero_height() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("zero-height.gif"); + } + + function test_zero_size() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("zero-size.gif"); + } + + function test_zero_width() { + $this -> expectException(Exception::class); + $img = $this -> loadImage("zero-width.gif"); + } +} + diff --git a/test/resources/pygif/255-codes.gif b/test/resources/pygif/255-codes.gif new file mode 100644 index 0000000..8714a69 Binary files /dev/null and b/test/resources/pygif/255-codes.gif differ diff --git a/test/resources/pygif/4095-codes-clear.gif b/test/resources/pygif/4095-codes-clear.gif new file mode 100644 index 0000000..4c9af9b Binary files /dev/null and b/test/resources/pygif/4095-codes-clear.gif differ diff --git a/test/resources/pygif/4095-codes.gif b/test/resources/pygif/4095-codes.gif new file mode 100644 index 0000000..e1def5a Binary files /dev/null and b/test/resources/pygif/4095-codes.gif differ diff --git a/test/resources/pygif/PyGIF.LICENSE b/test/resources/pygif/PyGIF.LICENSE new file mode 100644 index 0000000..f9de7c0 --- /dev/null +++ b/test/resources/pygif/PyGIF.LICENSE @@ -0,0 +1,5 @@ +The test inputs in this directory are from the PyGIF test suite, which is (c) 2018 Robert Ancell. + +PyGIF is licensed under the GNU Lesser General Public License Version 3. + +https://github.com/robert-ancell/pygif diff --git a/test/resources/pygif/all-blues.gif b/test/resources/pygif/all-blues.gif new file mode 100644 index 0000000..c528d16 Binary files /dev/null and b/test/resources/pygif/all-blues.gif differ diff --git a/test/resources/pygif/all-greens.gif b/test/resources/pygif/all-greens.gif new file mode 100644 index 0000000..0cf1a7e Binary files /dev/null and b/test/resources/pygif/all-greens.gif differ diff --git a/test/resources/pygif/all-reds.gif b/test/resources/pygif/all-reds.gif new file mode 100644 index 0000000..dacf261 Binary files /dev/null and b/test/resources/pygif/all-reds.gif differ diff --git a/test/resources/pygif/animation-multi-image-explicit-zero-delay.gif b/test/resources/pygif/animation-multi-image-explicit-zero-delay.gif new file mode 100644 index 0000000..d3bcb7b Binary files /dev/null and b/test/resources/pygif/animation-multi-image-explicit-zero-delay.gif differ diff --git a/test/resources/pygif/animation-multi-image.gif b/test/resources/pygif/animation-multi-image.gif new file mode 100644 index 0000000..4906098 Binary files /dev/null and b/test/resources/pygif/animation-multi-image.gif differ diff --git a/test/resources/pygif/animation-no-delays.gif b/test/resources/pygif/animation-no-delays.gif new file mode 100644 index 0000000..a95e242 Binary files /dev/null and b/test/resources/pygif/animation-no-delays.gif differ diff --git a/test/resources/pygif/animation-speed.gif b/test/resources/pygif/animation-speed.gif new file mode 100644 index 0000000..eb27611 Binary files /dev/null and b/test/resources/pygif/animation-speed.gif differ diff --git a/test/resources/pygif/animation-zero-delays.gif b/test/resources/pygif/animation-zero-delays.gif new file mode 100644 index 0000000..9c74fdb Binary files /dev/null and b/test/resources/pygif/animation-zero-delays.gif differ diff --git a/test/resources/pygif/animation.gif b/test/resources/pygif/animation.gif new file mode 100644 index 0000000..3cd5276 Binary files /dev/null and b/test/resources/pygif/animation.gif differ diff --git a/test/resources/pygif/comment.gif b/test/resources/pygif/comment.gif new file mode 100644 index 0000000..52b801f Binary files /dev/null and b/test/resources/pygif/comment.gif differ diff --git a/test/resources/pygif/depth1.gif b/test/resources/pygif/depth1.gif new file mode 100644 index 0000000..d3b1a94 Binary files /dev/null and b/test/resources/pygif/depth1.gif differ diff --git a/test/resources/pygif/depth2.gif b/test/resources/pygif/depth2.gif new file mode 100644 index 0000000..a003934 Binary files /dev/null and b/test/resources/pygif/depth2.gif differ diff --git a/test/resources/pygif/depth3.gif b/test/resources/pygif/depth3.gif new file mode 100644 index 0000000..7911fcc Binary files /dev/null and b/test/resources/pygif/depth3.gif differ diff --git a/test/resources/pygif/depth4.gif b/test/resources/pygif/depth4.gif new file mode 100644 index 0000000..8fccd08 Binary files /dev/null and b/test/resources/pygif/depth4.gif differ diff --git a/test/resources/pygif/depth5.gif b/test/resources/pygif/depth5.gif new file mode 100644 index 0000000..e61ccb9 Binary files /dev/null and b/test/resources/pygif/depth5.gif differ diff --git a/test/resources/pygif/depth6.gif b/test/resources/pygif/depth6.gif new file mode 100644 index 0000000..0274106 Binary files /dev/null and b/test/resources/pygif/depth6.gif differ diff --git a/test/resources/pygif/depth7.gif b/test/resources/pygif/depth7.gif new file mode 100644 index 0000000..e31caa2 Binary files /dev/null and b/test/resources/pygif/depth7.gif differ diff --git a/test/resources/pygif/depth8.gif b/test/resources/pygif/depth8.gif new file mode 100644 index 0000000..f2b1496 Binary files /dev/null and b/test/resources/pygif/depth8.gif differ diff --git a/test/resources/pygif/disabled-transparent.gif b/test/resources/pygif/disabled-transparent.gif new file mode 100644 index 0000000..dbe0e54 Binary files /dev/null and b/test/resources/pygif/disabled-transparent.gif differ diff --git a/test/resources/pygif/dispose-keep.gif b/test/resources/pygif/dispose-keep.gif new file mode 100644 index 0000000..7e77bb8 Binary files /dev/null and b/test/resources/pygif/dispose-keep.gif differ diff --git a/test/resources/pygif/dispose-none.gif b/test/resources/pygif/dispose-none.gif new file mode 100644 index 0000000..66c346b Binary files /dev/null and b/test/resources/pygif/dispose-none.gif differ diff --git a/test/resources/pygif/dispose-restore-background.gif b/test/resources/pygif/dispose-restore-background.gif new file mode 100644 index 0000000..4ee2622 Binary files /dev/null and b/test/resources/pygif/dispose-restore-background.gif differ diff --git a/test/resources/pygif/dispose-restore-previous.gif b/test/resources/pygif/dispose-restore-previous.gif new file mode 100644 index 0000000..61ac3af Binary files /dev/null and b/test/resources/pygif/dispose-restore-previous.gif differ diff --git a/test/resources/pygif/double-clears.gif b/test/resources/pygif/double-clears.gif new file mode 100644 index 0000000..dcf54c6 Binary files /dev/null and b/test/resources/pygif/double-clears.gif differ diff --git a/test/resources/pygif/extra-data.gif b/test/resources/pygif/extra-data.gif new file mode 100644 index 0000000..d49327d Binary files /dev/null and b/test/resources/pygif/extra-data.gif differ diff --git a/test/resources/pygif/extra-pixels.gif b/test/resources/pygif/extra-pixels.gif new file mode 100644 index 0000000..b9bf451 Binary files /dev/null and b/test/resources/pygif/extra-pixels.gif differ diff --git a/test/resources/pygif/four-colors.gif b/test/resources/pygif/four-colors.gif new file mode 100644 index 0000000..15d0d1c Binary files /dev/null and b/test/resources/pygif/four-colors.gif differ diff --git a/test/resources/pygif/gif87a-animation.gif b/test/resources/pygif/gif87a-animation.gif new file mode 100644 index 0000000..900c4e5 Binary files /dev/null and b/test/resources/pygif/gif87a-animation.gif differ diff --git a/test/resources/pygif/gif87a.gif b/test/resources/pygif/gif87a.gif new file mode 100644 index 0000000..628ee88 Binary files /dev/null and b/test/resources/pygif/gif87a.gif differ diff --git a/test/resources/pygif/high-color.gif b/test/resources/pygif/high-color.gif new file mode 100644 index 0000000..3e2af18 Binary files /dev/null and b/test/resources/pygif/high-color.gif differ diff --git a/test/resources/pygif/icc-color-profile-empty.gif b/test/resources/pygif/icc-color-profile-empty.gif new file mode 100644 index 0000000..db64048 Binary files /dev/null and b/test/resources/pygif/icc-color-profile-empty.gif differ diff --git a/test/resources/pygif/icc-color-profile.gif b/test/resources/pygif/icc-color-profile.gif new file mode 100644 index 0000000..d0b1126 Binary files /dev/null and b/test/resources/pygif/icc-color-profile.gif differ diff --git a/test/resources/pygif/image-inside-bg.gif b/test/resources/pygif/image-inside-bg.gif new file mode 100644 index 0000000..1820049 Binary files /dev/null and b/test/resources/pygif/image-inside-bg.gif differ diff --git a/test/resources/pygif/image-outside-bg.gif b/test/resources/pygif/image-outside-bg.gif new file mode 100644 index 0000000..df74b41 Binary files /dev/null and b/test/resources/pygif/image-outside-bg.gif differ diff --git a/test/resources/pygif/image-overlap-bg.gif b/test/resources/pygif/image-overlap-bg.gif new file mode 100644 index 0000000..a20ee24 Binary files /dev/null and b/test/resources/pygif/image-overlap-bg.gif differ diff --git a/test/resources/pygif/image-zero-height.gif b/test/resources/pygif/image-zero-height.gif new file mode 100644 index 0000000..ddd3c43 Binary files /dev/null and b/test/resources/pygif/image-zero-height.gif differ diff --git a/test/resources/pygif/image-zero-size.gif b/test/resources/pygif/image-zero-size.gif new file mode 100644 index 0000000..8cb8c9e Binary files /dev/null and b/test/resources/pygif/image-zero-size.gif differ diff --git a/test/resources/pygif/image-zero-width.gif b/test/resources/pygif/image-zero-width.gif new file mode 100644 index 0000000..ba61320 Binary files /dev/null and b/test/resources/pygif/image-zero-width.gif differ diff --git a/test/resources/pygif/images-combine.gif b/test/resources/pygif/images-combine.gif new file mode 100644 index 0000000..288e589 Binary files /dev/null and b/test/resources/pygif/images-combine.gif differ diff --git a/test/resources/pygif/images-overlap.gif b/test/resources/pygif/images-overlap.gif new file mode 100644 index 0000000..6dd1b2a Binary files /dev/null and b/test/resources/pygif/images-overlap.gif differ diff --git a/test/resources/pygif/interlace.gif b/test/resources/pygif/interlace.gif new file mode 100644 index 0000000..c2116d8 Binary files /dev/null and b/test/resources/pygif/interlace.gif differ diff --git a/test/resources/pygif/invalid-ascii-comment.gif b/test/resources/pygif/invalid-ascii-comment.gif new file mode 100644 index 0000000..a0398af Binary files /dev/null and b/test/resources/pygif/invalid-ascii-comment.gif differ diff --git a/test/resources/pygif/invalid-background.gif b/test/resources/pygif/invalid-background.gif new file mode 100644 index 0000000..4943c72 Binary files /dev/null and b/test/resources/pygif/invalid-background.gif differ diff --git a/test/resources/pygif/invalid-code.gif b/test/resources/pygif/invalid-code.gif new file mode 100644 index 0000000..7d929c9 Binary files /dev/null and b/test/resources/pygif/invalid-code.gif differ diff --git a/test/resources/pygif/invalid-colors.gif b/test/resources/pygif/invalid-colors.gif new file mode 100644 index 0000000..c311152 Binary files /dev/null and b/test/resources/pygif/invalid-colors.gif differ diff --git a/test/resources/pygif/invalid-transparent.gif b/test/resources/pygif/invalid-transparent.gif new file mode 100644 index 0000000..ce02c1a Binary files /dev/null and b/test/resources/pygif/invalid-transparent.gif differ diff --git a/test/resources/pygif/invalid-utf8-comment.gif b/test/resources/pygif/invalid-utf8-comment.gif new file mode 100644 index 0000000..893df11 Binary files /dev/null and b/test/resources/pygif/invalid-utf8-comment.gif differ diff --git a/test/resources/pygif/large-codes.gif b/test/resources/pygif/large-codes.gif new file mode 100644 index 0000000..15ae2f4 Binary files /dev/null and b/test/resources/pygif/large-codes.gif differ diff --git a/test/resources/pygif/large-comment.gif b/test/resources/pygif/large-comment.gif new file mode 100644 index 0000000..8991742 Binary files /dev/null and b/test/resources/pygif/large-comment.gif differ diff --git a/test/resources/pygif/local-color-table.gif b/test/resources/pygif/local-color-table.gif new file mode 100644 index 0000000..7b64e43 Binary files /dev/null and b/test/resources/pygif/local-color-table.gif differ diff --git a/test/resources/pygif/loop-animexts.gif b/test/resources/pygif/loop-animexts.gif new file mode 100644 index 0000000..502cf17 Binary files /dev/null and b/test/resources/pygif/loop-animexts.gif differ diff --git a/test/resources/pygif/loop-buffer.gif b/test/resources/pygif/loop-buffer.gif new file mode 100644 index 0000000..148ee08 Binary files /dev/null and b/test/resources/pygif/loop-buffer.gif differ diff --git a/test/resources/pygif/loop-buffer_max.gif b/test/resources/pygif/loop-buffer_max.gif new file mode 100644 index 0000000..548c236 Binary files /dev/null and b/test/resources/pygif/loop-buffer_max.gif differ diff --git a/test/resources/pygif/loop-infinite.gif b/test/resources/pygif/loop-infinite.gif new file mode 100644 index 0000000..7471d57 Binary files /dev/null and b/test/resources/pygif/loop-infinite.gif differ diff --git a/test/resources/pygif/loop-max.gif b/test/resources/pygif/loop-max.gif new file mode 100644 index 0000000..1f9657f Binary files /dev/null and b/test/resources/pygif/loop-max.gif differ diff --git a/test/resources/pygif/loop-once.gif b/test/resources/pygif/loop-once.gif new file mode 100644 index 0000000..38fb359 Binary files /dev/null and b/test/resources/pygif/loop-once.gif differ diff --git a/test/resources/pygif/many-clears.gif b/test/resources/pygif/many-clears.gif new file mode 100644 index 0000000..3adcb96 Binary files /dev/null and b/test/resources/pygif/many-clears.gif differ diff --git a/test/resources/pygif/max-codes.gif b/test/resources/pygif/max-codes.gif new file mode 100644 index 0000000..bd3f8d2 Binary files /dev/null and b/test/resources/pygif/max-codes.gif differ diff --git a/test/resources/pygif/max-height.gif b/test/resources/pygif/max-height.gif new file mode 100644 index 0000000..586d030 Binary files /dev/null and b/test/resources/pygif/max-height.gif differ diff --git a/test/resources/pygif/max-size.gif b/test/resources/pygif/max-size.gif new file mode 100644 index 0000000..e408b69 Binary files /dev/null and b/test/resources/pygif/max-size.gif differ diff --git a/test/resources/pygif/max-width.gif b/test/resources/pygif/max-width.gif new file mode 100644 index 0000000..6ec2fdd Binary files /dev/null and b/test/resources/pygif/max-width.gif differ diff --git a/test/resources/pygif/missing-pixels.gif b/test/resources/pygif/missing-pixels.gif new file mode 100644 index 0000000..aa21ccd Binary files /dev/null and b/test/resources/pygif/missing-pixels.gif differ diff --git a/test/resources/pygif/no-clear-and-eoi.gif b/test/resources/pygif/no-clear-and-eoi.gif new file mode 100644 index 0000000..3e90808 Binary files /dev/null and b/test/resources/pygif/no-clear-and-eoi.gif differ diff --git a/test/resources/pygif/no-clear.gif b/test/resources/pygif/no-clear.gif new file mode 100644 index 0000000..62b7271 Binary files /dev/null and b/test/resources/pygif/no-clear.gif differ diff --git a/test/resources/pygif/no-data.gif b/test/resources/pygif/no-data.gif new file mode 100644 index 0000000..bb3d1f7 Binary files /dev/null and b/test/resources/pygif/no-data.gif differ diff --git a/test/resources/pygif/no-eoi.gif b/test/resources/pygif/no-eoi.gif new file mode 100644 index 0000000..d3e5f19 Binary files /dev/null and b/test/resources/pygif/no-eoi.gif differ diff --git a/test/resources/pygif/no-global-color-table.gif b/test/resources/pygif/no-global-color-table.gif new file mode 100644 index 0000000..f8052af Binary files /dev/null and b/test/resources/pygif/no-global-color-table.gif differ diff --git a/test/resources/pygif/nul-application-extension.gif b/test/resources/pygif/nul-application-extension.gif new file mode 100644 index 0000000..176ae31 Binary files /dev/null and b/test/resources/pygif/nul-application-extension.gif differ diff --git a/test/resources/pygif/nul-comment.gif b/test/resources/pygif/nul-comment.gif new file mode 100644 index 0000000..a002468 Binary files /dev/null and b/test/resources/pygif/nul-comment.gif differ diff --git a/test/resources/pygif/plain-text.gif b/test/resources/pygif/plain-text.gif new file mode 100644 index 0000000..5be1cb0 Binary files /dev/null and b/test/resources/pygif/plain-text.gif differ diff --git a/test/resources/pygif/transparent.gif b/test/resources/pygif/transparent.gif new file mode 100644 index 0000000..d8be13e Binary files /dev/null and b/test/resources/pygif/transparent.gif differ diff --git a/test/resources/pygif/unknown-application-extension.gif b/test/resources/pygif/unknown-application-extension.gif new file mode 100644 index 0000000..9204606 Binary files /dev/null and b/test/resources/pygif/unknown-application-extension.gif differ diff --git a/test/resources/pygif/unknown-extension.gif b/test/resources/pygif/unknown-extension.gif new file mode 100644 index 0000000..8518ff6 Binary files /dev/null and b/test/resources/pygif/unknown-extension.gif differ diff --git a/test/resources/pygif/xmp-data-empty.gif b/test/resources/pygif/xmp-data-empty.gif new file mode 100644 index 0000000..6a7a23d Binary files /dev/null and b/test/resources/pygif/xmp-data-empty.gif differ diff --git a/test/resources/pygif/xmp-data.gif b/test/resources/pygif/xmp-data.gif new file mode 100644 index 0000000..80535f7 Binary files /dev/null and b/test/resources/pygif/xmp-data.gif differ diff --git a/test/resources/pygif/zero-height.gif b/test/resources/pygif/zero-height.gif new file mode 100644 index 0000000..5c5a351 Binary files /dev/null and b/test/resources/pygif/zero-height.gif differ diff --git a/test/resources/pygif/zero-size.gif b/test/resources/pygif/zero-size.gif new file mode 100644 index 0000000..52d939e Binary files /dev/null and b/test/resources/pygif/zero-size.gif differ diff --git a/test/resources/pygif/zero-width.gif b/test/resources/pygif/zero-width.gif new file mode 100644 index 0000000..0c965d4 Binary files /dev/null and b/test/resources/pygif/zero-width.gif differ diff --git a/test/unit/Codec/PngCodecTest.php b/test/unit/Codec/PngCodecTest.php index ef25554..a493de5 100644 --- a/test/unit/Codec/PngCodecTest.php +++ b/test/unit/Codec/PngCodecTest.php @@ -50,7 +50,7 @@ public function testBlackAndWhiteImageLoad() { $interlacedImage = Image::fromFile(__DIR__ . "/../../resources/pngsuite/basi0g01.png"); $interlacedResult = $interlacedImage -> toString(); $nonInterlacedImage = Image::fromFile(__DIR__ . "/../../resources/pngsuite/basn0g01.png"); - $nonInterlacedResult = $interlacedImage -> toString(); + $nonInterlacedResult = $nonInterlacedImage -> toString(); // These should both match the expected output $this -> assertEquals($expected, $interlacedResult); $this -> assertEquals($expected, $nonInterlacedResult);