From bf7c4208f283240bb4b311e1a39294f6eb00e069 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 00:40:13 +1100 Subject: [PATCH 1/7] implement reading of basic GIF files --- .../GfxPhp/Codec/Gif/GifApplicationExt.php | 15 +++ src/Mike42/GfxPhp/Codec/Gif/GifColorTable.php | 26 +++++ src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php | 15 +++ src/Mike42/GfxPhp/Codec/Gif/GifData.php | 80 ++++++++++++++++ src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php | 82 ++++++++++++++++ .../GfxPhp/Codec/Gif/GifGraphicControlExt.php | 20 ++++ .../GfxPhp/Codec/Gif/GifGraphicsBlock.php | 62 ++++++++++++ .../GfxPhp/Codec/Gif/GifImageDescriptor.php | 94 +++++++++++++++++++ .../GfxPhp/Codec/Gif/GifLogicalScreen.php | 40 ++++++++ .../Codec/Gif/GifLogicalScreenDescriptor.php | 90 ++++++++++++++++++ .../GfxPhp/Codec/Gif/GifPlaintextExt.php | 15 +++ .../Codec/Gif/GifSpecialPurposeBlock.php | 27 ++++++ .../GfxPhp/Codec/Gif/GifTableBasedImage.php | 57 +++++++++++ .../GfxPhp/Codec/Gif/GifUnrecognisedExt.php | 15 +++ src/Mike42/GfxPhp/Codec/GifCodec.php | 27 +++++- src/Mike42/GfxPhp/Codec/ImageCodec.php | 1 + 16 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifColorTable.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifData.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifImageDescriptor.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreen.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifLogicalScreenDescriptor.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifSpecialPurposeBlock.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifTableBasedImage.php create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php new file mode 100644 index 0000000..72ec790 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php @@ -0,0 +1,15 @@ + 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..bb80216 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php @@ -0,0 +1,15 @@ +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 = GifUnrecognisedExt::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..8f58566 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -0,0 +1,82 @@ + 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()); + } + $currentIndex++; + } + } + throw new \Exception("Could not find image #$imageIndex in GIF file"); + } + + private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBasedImage $tableBasedImage) : 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()); + // Array of ints for IndexedRasterImage + $dataArr = array_values(unpack("C*", $decompressedData)); + return IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php new file mode 100644 index 0000000..0a29b74 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php @@ -0,0 +1,20 @@ + read(8); + return new GifGraphicControlExt(); + } +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php new file mode 100644 index 0000000..55d0243 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -0,0 +1,62 @@ +graphicControlExt = $graphicControlExt; + $this->tableBasedImage = $tableBasedImage; + $this->plaintextExt = $plaintextExt; + } + + public function getGraphicControlExt(): GifGraphicControlExt + { + return $this->graphicControlExt; + } + + public function getTableBasedImage(): GifTableBasedImage + { + return $this->tableBasedImage; + } + + public function getPlaintextExt(): GifPlaintextExt + { + 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_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..cc5569e --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php @@ -0,0 +1,15 @@ +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/GifUnrecognisedExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php new file mode 100644 index 0000000..76b24dc --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php @@ -0,0 +1,15 @@ + 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); From a0efada00340cde085d075866fe22b5773c95489 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 01:33:30 +1100 Subject: [PATCH 2/7] implement de-interlace, also more correctly ignore some blocks that aren't needed --- .../Codec/Common/DataBlobInputStream.php | 2 +- .../GfxPhp/Codec/Gif/GifApplicationExt.php | 42 +++++++++++++++- src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php | 19 ++++++- src/Mike42/GfxPhp/Codec/Gif/GifData.php | 4 +- src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php | 34 +++++++++++-- .../GfxPhp/Codec/Gif/GifGraphicsBlock.php | 13 ++--- .../GfxPhp/Codec/Gif/GifPlaintextExt.php | 39 ++++++++++++++- src/Mike42/GfxPhp/Codec/Gif/GifUnknownExt.php | 50 +++++++++++++++++++ .../GfxPhp/Codec/Gif/GifUnrecognisedExt.php | 15 ------ 9 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifUnknownExt.php delete mode 100644 src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php 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 index 72ec790..85a4fb2 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php @@ -7,9 +7,47 @@ class GifApplicationExt { + private $appIdentifer; + private $appAuthCode; + private $data; - public static function fromBin(DataInputStream $in) + public function __construct(string $appIdentifer, string $appAuthCode, array $data) { - throw new \Exception("GIF_EXTENSION_APPLICATION not implemented"); + $this->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/GifCommentExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php index bb80216..8c6a64e 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifCommentExt.php @@ -7,9 +7,26 @@ class GifCommentExt { + private $data; + + public function getData(): array + { + return $this->data; + } + + public function __construct(array $data) + { + $this -> data = $data; + } public static function fromBin(DataInputStream $in) { - throw new \Exception("GIF_EXTENSION_COMMENT not implemented"); + $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 index a4434b4..238e46b 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifData.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifData.php @@ -18,7 +18,7 @@ class GifData private $specialPurposeBlock; private $unrecognisedBlock; - public function __construct(GifGraphicsBlock $graphicsBlock = null, GifSpecialPurposeBlock $specialPurposeBlock = null, GifUnrecognisedExt $unrecognisedBlock = null) + public function __construct(GifGraphicsBlock $graphicsBlock = null, GifSpecialPurposeBlock $specialPurposeBlock = null, GifUnknownExt $unrecognisedBlock = null) { $this->graphicsBlock = $graphicsBlock; $this->specialPurposeBlock = $specialPurposeBlock; @@ -59,7 +59,7 @@ public static function fromBin(DataInputStream $in) : GifData } // Unknown extension blocks if ($blockId == GifData::GIF_EXTENSION && $extensionId != GifData::GIF_EXTENSION_GRAPHIC_CONTROL && $extensionId != GifData::GIF_EXTENSION_PLAINTEXT) { - $unrecognisedBlock = GifUnrecognisedExt::fromBin($in); + $unrecognisedBlock = GifUnknownExt::fromBin($in); return new GifData(null, null, $unrecognisedBlock); } $graphicsBlock = GifGraphicsBlock::fromBin($in); diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php index 8f58566..f88d2b9 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -3,9 +3,7 @@ use Mike42\GfxPhp\Codec\Common\DataInputStream; use Mike42\GfxPhp\IndexedRasterImage; -use Mike42\GfxPhp\RasterImage; use Mike42\GfxPhp\Util\LzwCompression; -use mysql_xdevapi\Exception; class GifDataStream { @@ -72,11 +70,39 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa 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 + // De-compress the actual image data $compressedData = join($tableBasedImage ->getDataSubBlocks()); $decompressedData = LzwCompression::decompress($compressedData, $tableBasedImage -> getLzqMinSize()); - // Array of ints for IndexedRasterImage + if($tableBasedImage -> getImageDescriptor() -> isInterlaced()) { + $decompressedData = self::deinterlace($width, $decompressedData); + } + // Array of ints for IndexedRasterImage $dataArr = array_values(unpack("C*", $decompressedData)); return IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); } + + 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/GifGraphicsBlock.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php index 55d0243..8a7cb6b 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -3,6 +3,7 @@ namespace Mike42\GfxPhp\Codec\Gif; + use Mike42\GfxPhp\Codec\Common\DataInputStream; class GifGraphicsBlock @@ -18,17 +19,17 @@ public function __construct(GifGraphicControlExt $graphicControlExt = null, GifT $this->plaintextExt = $plaintextExt; } - public function getGraphicControlExt(): GifGraphicControlExt + public function getGraphicControlExt() { return $this->graphicControlExt; } - public function getTableBasedImage(): GifTableBasedImage + public function getTableBasedImage() { return $this->tableBasedImage; } - public function getPlaintextExt(): GifPlaintextExt + public function getPlaintextExt() { return $this->plaintextExt; } @@ -40,7 +41,7 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock $extensionId = $peek[1]; // Could have a graphic control extension before it $graphicControlExtension = null; - if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { + if($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { // Optional graphic control extension $graphicControlExtension = GifGraphicControlExt::fromBin($in); // Re-populate for next block @@ -52,11 +53,11 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock // Plain text $plaintextExtension = GifPlaintextExt::fromBin($in); return new GifGraphicsBlock($graphicControlExtension, null, $plaintextExtension); - } else if ($blockId == GifData::GIF_IMAGE_SEPARATOR) { + } 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"); } -} +} \ No newline at end of file diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php index cc5569e..459774c 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php @@ -3,13 +3,48 @@ namespace Mike42\GfxPhp\Codec\Gif; + use Mike42\GfxPhp\Codec\Common\DataInputStream; class GifPlaintextExt { + private $header; + + public function getHeader(): string + { + return $this->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 { - throw new \Exception("GIF_EXTENSION_PLAINTEXT not implemented"); + $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); } -} + + +} \ No newline at end of file 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/Gif/GifUnrecognisedExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php deleted file mode 100644 index 76b24dc..0000000 --- a/src/Mike42/GfxPhp/Codec/Gif/GifUnrecognisedExt.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Sun, 17 Feb 2019 01:36:05 +1100 Subject: [PATCH 3/7] phpcs fixes --- src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php | 2 +- src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php | 13 +++++++------ src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php | 7 +++---- src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php | 7 ++----- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php index 85a4fb2..cf861ea 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifApplicationExt.php @@ -42,7 +42,7 @@ public static function fromBin(DataInputStream $in) : GifApplicationExt } $lenData = $in->read(1); $len = unpack("C", $lenData)[1]; - if($len != 11) { + if ($len != 11) { throw new \Exception("Incorrect size on application extension block"); } $appIdentifier = $in->read(8); diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php index f88d2b9..5da49bd 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -73,7 +73,7 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa // De-compress the actual image data $compressedData = join($tableBasedImage ->getDataSubBlocks()); $decompressedData = LzwCompression::decompress($compressedData, $tableBasedImage -> getLzqMinSize()); - if($tableBasedImage -> getImageDescriptor() -> isInterlaced()) { + if ($tableBasedImage -> getImageDescriptor() -> isInterlaced()) { $decompressedData = self::deinterlace($width, $decompressedData); } // Array of ints for IndexedRasterImage @@ -81,25 +81,26 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa return IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); } - private static function deinterlace(int $width, string $data) : string { + 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) { + for ($i = 0; $i < $height; $i += 8) { $new[$i] = $old[$j]; $j++; } - for($i = 4; $i < $height; $i += 8) { + for ($i = 4; $i < $height; $i += 8) { $new[$i] = $old[$j]; $j++; } - for($i = 2; $i < $height; $i += 4) { + for ($i = 2; $i < $height; $i += 4) { $new[$i] = $old[$j]; $j++; } - for($i = 1; $i < $height; $i += 2) { + for ($i = 1; $i < $height; $i += 2) { $new[$i] = $old[$j]; $j++; } diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php index 8a7cb6b..23d28ca 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -3,7 +3,6 @@ namespace Mike42\GfxPhp\Codec\Gif; - use Mike42\GfxPhp\Codec\Common\DataInputStream; class GifGraphicsBlock @@ -41,7 +40,7 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock $extensionId = $peek[1]; // Could have a graphic control extension before it $graphicControlExtension = null; - if($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { + if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) { // Optional graphic control extension $graphicControlExtension = GifGraphicControlExt::fromBin($in); // Re-populate for next block @@ -53,11 +52,11 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock // Plain text $plaintextExtension = GifPlaintextExt::fromBin($in); return new GifGraphicsBlock($graphicControlExtension, null, $plaintextExtension); - } else if($blockId == GifData::GIF_IMAGE_SEPARATOR) { + } 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"); } -} \ No newline at end of file +} diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php index 459774c..27c6d63 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifPlaintextExt.php @@ -3,7 +3,6 @@ namespace Mike42\GfxPhp\Codec\Gif; - use Mike42\GfxPhp\Codec\Common\DataInputStream; class GifPlaintextExt @@ -37,7 +36,7 @@ public static function fromBin(DataInputStream $in) : GifPlaintextExt } $lenData = $in->read(1); $len = unpack("C", $lenData)[1]; - if($len != 12) { + 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. @@ -45,6 +44,4 @@ public static function fromBin(DataInputStream $in) : GifPlaintextExt $data = GifData::readDataSubBlocks($in); return new GifPlaintextExt($header, $data); } - - -} \ No newline at end of file +} From 53b9adbd0536c142334fbd6245eb3160ea997fe9 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 14:55:04 +1100 Subject: [PATCH 4/7] include GIF tests, update documentation, fix PNG format test bug --- README.md | 72 +-- docs/user/formats.rst | 10 + test/integration/GifsuiteTest.php | 496 ++++++++++++++++++ test/resources/pygif/255-codes.gif | Bin 0 -> 6869 bytes test/resources/pygif/4095-codes-clear.gif | Bin 0 -> 6213 bytes test/resources/pygif/4095-codes.gif | Bin 0 -> 6146 bytes test/resources/pygif/PyGIF.LICENSE | 5 + test/resources/pygif/all-blues.gif | Bin 0 -> 1087 bytes test/resources/pygif/all-greens.gif | Bin 0 -> 1087 bytes test/resources/pygif/all-reds.gif | Bin 0 -> 1087 bytes ...mation-multi-image-explicit-zero-delay.gif | Bin 0 -> 206 bytes .../resources/pygif/animation-multi-image.gif | Bin 0 -> 182 bytes test/resources/pygif/animation-no-delays.gif | Bin 0 -> 101 bytes test/resources/pygif/animation-speed.gif | Bin 0 -> 133 bytes .../resources/pygif/animation-zero-delays.gif | Bin 0 -> 133 bytes test/resources/pygif/animation.gif | Bin 0 -> 133 bytes test/resources/pygif/comment.gif | Bin 0 -> 69 bytes test/resources/pygif/depth1.gif | Bin 0 -> 35 bytes test/resources/pygif/depth2.gif | Bin 0 -> 41 bytes test/resources/pygif/depth3.gif | Bin 0 -> 53 bytes test/resources/pygif/depth4.gif | Bin 0 -> 77 bytes test/resources/pygif/depth5.gif | Bin 0 -> 126 bytes test/resources/pygif/depth6.gif | Bin 0 -> 222 bytes test/resources/pygif/depth7.gif | Bin 0 -> 414 bytes test/resources/pygif/depth8.gif | Bin 0 -> 799 bytes test/resources/pygif/disabled-transparent.gif | Bin 0 -> 62 bytes test/resources/pygif/dispose-keep.gif | Bin 0 -> 131 bytes test/resources/pygif/dispose-none.gif | Bin 0 -> 131 bytes .../pygif/dispose-restore-background.gif | Bin 0 -> 131 bytes .../pygif/dispose-restore-previous.gif | Bin 0 -> 146 bytes test/resources/pygif/double-clears.gif | Bin 0 -> 148 bytes test/resources/pygif/extra-data.gif | Bin 0 -> 68 bytes test/resources/pygif/extra-pixels.gif | Bin 0 -> 60 bytes test/resources/pygif/four-colors.gif | Bin 0 -> 58 bytes test/resources/pygif/gif87a-animation.gif | Bin 0 -> 82 bytes test/resources/pygif/gif87a.gif | Bin 0 -> 35 bytes test/resources/pygif/high-color.gif | Bin 0 -> 4306 bytes .../pygif/icc-color-profile-empty.gif | Bin 0 -> 68 bytes test/resources/pygif/icc-color-profile.gif | Bin 0 -> 16822 bytes test/resources/pygif/image-inside-bg.gif | Bin 0 -> 53 bytes test/resources/pygif/image-outside-bg.gif | Bin 0 -> 54 bytes test/resources/pygif/image-overlap-bg.gif | Bin 0 -> 54 bytes test/resources/pygif/image-zero-height.gif | Bin 0 -> 30 bytes test/resources/pygif/image-zero-size.gif | Bin 0 -> 30 bytes test/resources/pygif/image-zero-width.gif | Bin 0 -> 30 bytes test/resources/pygif/images-combine.gif | Bin 0 -> 98 bytes test/resources/pygif/images-overlap.gif | Bin 0 -> 68 bytes test/resources/pygif/interlace.gif | Bin 0 -> 1087 bytes .../resources/pygif/invalid-ascii-comment.gif | Bin 0 -> 59 bytes test/resources/pygif/invalid-background.gif | Bin 0 -> 35 bytes test/resources/pygif/invalid-code.gif | Bin 0 -> 35 bytes test/resources/pygif/invalid-colors.gif | Bin 0 -> 37 bytes test/resources/pygif/invalid-transparent.gif | Bin 0 -> 62 bytes test/resources/pygif/invalid-utf8-comment.gif | Bin 0 -> 60 bytes test/resources/pygif/large-codes.gif | Bin 0 -> 6358 bytes test/resources/pygif/large-comment.gif | Bin 0 -> 13106 bytes test/resources/pygif/local-color-table.gif | Bin 0 -> 41 bytes test/resources/pygif/loop-animexts.gif | Bin 0 -> 78 bytes test/resources/pygif/loop-buffer.gif | Bin 0 -> 78 bytes test/resources/pygif/loop-buffer_max.gif | Bin 0 -> 78 bytes test/resources/pygif/loop-infinite.gif | Bin 0 -> 72 bytes test/resources/pygif/loop-max.gif | Bin 0 -> 72 bytes test/resources/pygif/loop-once.gif | Bin 0 -> 72 bytes test/resources/pygif/many-clears.gif | Bin 0 -> 116 bytes test/resources/pygif/max-codes.gif | Bin 0 -> 7624 bytes test/resources/pygif/max-height.gif | Bin 0 -> 405 bytes test/resources/pygif/max-size.gif | Bin 0 -> 38 bytes test/resources/pygif/max-width.gif | Bin 0 -> 405 bytes test/resources/pygif/missing-pixels.gif | Bin 0 -> 53 bytes test/resources/pygif/no-clear-and-eoi.gif | Bin 0 -> 52 bytes test/resources/pygif/no-clear.gif | Bin 0 -> 52 bytes test/resources/pygif/no-data.gif | Bin 0 -> 20 bytes test/resources/pygif/no-eoi.gif | Bin 0 -> 52 bytes .../resources/pygif/no-global-color-table.gif | Bin 0 -> 35 bytes .../pygif/nul-application-extension.gif | Bin 0 -> 78 bytes test/resources/pygif/nul-comment.gif | Bin 0 -> 58 bytes test/resources/pygif/plain-text.gif | Bin 0 -> 90 bytes test/resources/pygif/transparent.gif | Bin 0 -> 62 bytes .../pygif/unknown-application-extension.gif | Bin 0 -> 80 bytes test/resources/pygif/unknown-extension.gif | Bin 0 -> 68 bytes test/resources/pygif/xmp-data-empty.gif | Bin 0 -> 325 bytes test/resources/pygif/xmp-data.gif | Bin 0 -> 659 bytes test/resources/pygif/zero-height.gif | Bin 0 -> 20 bytes test/resources/pygif/zero-size.gif | Bin 0 -> 20 bytes test/resources/pygif/zero-width.gif | Bin 0 -> 20 bytes test/unit/Codec/PngCodecTest.php | 2 +- 86 files changed, 549 insertions(+), 36 deletions(-) create mode 100644 test/integration/GifsuiteTest.php create mode 100644 test/resources/pygif/255-codes.gif create mode 100644 test/resources/pygif/4095-codes-clear.gif create mode 100644 test/resources/pygif/4095-codes.gif create mode 100644 test/resources/pygif/PyGIF.LICENSE create mode 100644 test/resources/pygif/all-blues.gif create mode 100644 test/resources/pygif/all-greens.gif create mode 100644 test/resources/pygif/all-reds.gif create mode 100644 test/resources/pygif/animation-multi-image-explicit-zero-delay.gif create mode 100644 test/resources/pygif/animation-multi-image.gif create mode 100644 test/resources/pygif/animation-no-delays.gif create mode 100644 test/resources/pygif/animation-speed.gif create mode 100644 test/resources/pygif/animation-zero-delays.gif create mode 100644 test/resources/pygif/animation.gif create mode 100644 test/resources/pygif/comment.gif create mode 100644 test/resources/pygif/depth1.gif create mode 100644 test/resources/pygif/depth2.gif create mode 100644 test/resources/pygif/depth3.gif create mode 100644 test/resources/pygif/depth4.gif create mode 100644 test/resources/pygif/depth5.gif create mode 100644 test/resources/pygif/depth6.gif create mode 100644 test/resources/pygif/depth7.gif create mode 100644 test/resources/pygif/depth8.gif create mode 100644 test/resources/pygif/disabled-transparent.gif create mode 100644 test/resources/pygif/dispose-keep.gif create mode 100644 test/resources/pygif/dispose-none.gif create mode 100644 test/resources/pygif/dispose-restore-background.gif create mode 100644 test/resources/pygif/dispose-restore-previous.gif create mode 100644 test/resources/pygif/double-clears.gif create mode 100644 test/resources/pygif/extra-data.gif create mode 100644 test/resources/pygif/extra-pixels.gif create mode 100644 test/resources/pygif/four-colors.gif create mode 100644 test/resources/pygif/gif87a-animation.gif create mode 100644 test/resources/pygif/gif87a.gif create mode 100644 test/resources/pygif/high-color.gif create mode 100644 test/resources/pygif/icc-color-profile-empty.gif create mode 100644 test/resources/pygif/icc-color-profile.gif create mode 100644 test/resources/pygif/image-inside-bg.gif create mode 100644 test/resources/pygif/image-outside-bg.gif create mode 100644 test/resources/pygif/image-overlap-bg.gif create mode 100644 test/resources/pygif/image-zero-height.gif create mode 100644 test/resources/pygif/image-zero-size.gif create mode 100644 test/resources/pygif/image-zero-width.gif create mode 100644 test/resources/pygif/images-combine.gif create mode 100644 test/resources/pygif/images-overlap.gif create mode 100644 test/resources/pygif/interlace.gif create mode 100644 test/resources/pygif/invalid-ascii-comment.gif create mode 100644 test/resources/pygif/invalid-background.gif create mode 100644 test/resources/pygif/invalid-code.gif create mode 100644 test/resources/pygif/invalid-colors.gif create mode 100644 test/resources/pygif/invalid-transparent.gif create mode 100644 test/resources/pygif/invalid-utf8-comment.gif create mode 100644 test/resources/pygif/large-codes.gif create mode 100644 test/resources/pygif/large-comment.gif create mode 100644 test/resources/pygif/local-color-table.gif create mode 100644 test/resources/pygif/loop-animexts.gif create mode 100644 test/resources/pygif/loop-buffer.gif create mode 100644 test/resources/pygif/loop-buffer_max.gif create mode 100644 test/resources/pygif/loop-infinite.gif create mode 100644 test/resources/pygif/loop-max.gif create mode 100644 test/resources/pygif/loop-once.gif create mode 100644 test/resources/pygif/many-clears.gif create mode 100644 test/resources/pygif/max-codes.gif create mode 100644 test/resources/pygif/max-height.gif create mode 100644 test/resources/pygif/max-size.gif create mode 100644 test/resources/pygif/max-width.gif create mode 100644 test/resources/pygif/missing-pixels.gif create mode 100644 test/resources/pygif/no-clear-and-eoi.gif create mode 100644 test/resources/pygif/no-clear.gif create mode 100644 test/resources/pygif/no-data.gif create mode 100644 test/resources/pygif/no-eoi.gif create mode 100644 test/resources/pygif/no-global-color-table.gif create mode 100644 test/resources/pygif/nul-application-extension.gif create mode 100644 test/resources/pygif/nul-comment.gif create mode 100644 test/resources/pygif/plain-text.gif create mode 100644 test/resources/pygif/transparent.gif create mode 100644 test/resources/pygif/unknown-application-extension.gif create mode 100644 test/resources/pygif/unknown-extension.gif create mode 100644 test/resources/pygif/xmp-data-empty.gif create mode 100644 test/resources/pygif/xmp-data.gif create mode 100644 test/resources/pygif/zero-height.gif create mode 100644 test/resources/pygif/zero-size.gif create mode 100644 test/resources/pygif/zero-width.gif 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/docs/user/formats.rst b/docs/user/formats.rst index b731db9..4a0e277 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 +- Information about transparency is currently not used + +A GIF image will alwasys be loaded into an isntance of :class:`IndexedRasterImage`, which makes palette information available. + Netpbm Formats ^^^^^^^^^^^^^^ diff --git a/test/integration/GifsuiteTest.php b/test/integration/GifsuiteTest.php new file mode 100644 index 0000000..e0834ea --- /dev/null +++ b/test/integration/GifsuiteTest.php @@ -0,0 +1,496 @@ + 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() { + $this -> markTestSkipped("Known bug: Extra pixels not correctly discarded"); + $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 0000000000000000000000000000000000000000..8714a69079306ff0f9c33c1a09d261882807b64a GIT binary patch literal 6869 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 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/4095-codes-clear.gif b/test/resources/pygif/4095-codes-clear.gif new file mode 100644 index 0000000000000000000000000000000000000000..4c9af9b21ed0741175b666a18411a18714d7d4f3 GIT binary patch literal 6213 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 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/4095-codes.gif b/test/resources/pygif/4095-codes.gif new file mode 100644 index 0000000000000000000000000000000000000000..e1def5a54d85bdca3b6c1e56f3c3a32f0a307bee GIT binary patch literal 6146 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 h0@6A_4Aj5` wB$#;Oz``IA9grGEAOs4BFd|eV3Hv~Vp{hY@v8aX%Bddm(jG-Dy7^vDB0K-`rQ~&?~ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/animation-multi-image.gif b/test/resources/pygif/animation-multi-image.gif new file mode 100644 index 0000000000000000000000000000000000000000..49060983be8b169b9cea58a1b4b5eaec3a086896 GIT binary patch literal 182 zcmZ?wbhEHbWMW`q_{abP|A7ERF)04$_Hzvhc6JPKHPSO+W&{c<{$yb>0@6A_4Aj5` rB$#;O7<52#j6ewFhA<*jAqo31f>c2uhAOx)NEO5+46~4gfvT(l3hWf_ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/animation-no-delays.gif b/test/resources/pygif/animation-no-delays.gif new file mode 100644 index 0000000000000000000000000000000000000000..a95e2427abfa30cd148c0633f7608cae28857826 GIT binary patch literal 101 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6x9JqffO+?G4aG9bD3Qj SSdsZmEy2iKW)>z^25SI&p$tv{ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/animation-speed.gif b/test/resources/pygif/animation-speed.gif new file mode 100644 index 0000000000000000000000000000000000000000..eb276119fd85299c3fea36918770f37bd5b0066c GIT binary patch literal 133 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m!XOExbwEl$iW!)g ic;djqMySHfE)1+-@f1{Xrj}r^@Cj65W)>z^25SHiH4;4l literal 0 HcmV?d00001 diff --git a/test/resources/pygif/animation-zero-delays.gif b/test/resources/pygif/animation-zero-delays.gif new file mode 100644 index 0000000000000000000000000000000000000000..9c74fdbd8e34f0004cb91f67622a694c25575559 GIT binary patch literal 133 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m0+!VQks!qkOiVm+ Y*o2u~7+A51GqnU`6J};%Vr8%f0N7&?#sB~S literal 0 HcmV?d00001 diff --git a/test/resources/pygif/animation.gif b/test/resources/pygif/animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..3cd5276142e0c4eae522fba10c487fa66d17b5cf GIT binary patch literal 133 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m!e9iXbwEl$iW!)g ac;c`LGrKUbViRX-3C1SO%)-RVU=08SG7*jd literal 0 HcmV?d00001 diff --git a/test/resources/pygif/comment.gif b/test/resources/pygif/comment.gif new file mode 100644 index 0000000000000000000000000000000000000000..52b801f82a856cef415103c1656e0ac7cb034291 GIT binary patch literal 69 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixWRJW_LV@)g4Ki*iyF8FWA@L3$XNnIt$F FtN}Fa6o>!- literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth1.gif b/test/resources/pygif/depth1.gif new file mode 100644 index 0000000000000000000000000000000000000000..d3b1a94c9ce4c26c8d5a6f2ee72304a316fd4891 GIT binary patch literal 35 jcmZ?wbhEHbWMp7u_`m=H|NsBj0ns241|}vSMh0sDho1&g literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth2.gif b/test/resources/pygif/depth2.gif new file mode 100644 index 0000000000000000000000000000000000000000..a0039348ca171668c096074196f425f9d23c0de4 GIT binary patch literal 41 qcmZ?wbhEHbWMp7u_{abPp`oFxR;~K~|Gy512NGvsVv1p8um%9xUI}0T literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth3.gif b/test/resources/pygif/depth3.gif new file mode 100644 index 0000000000000000000000000000000000000000..7911fcc168128fc414767a4f4ac5ffb5df683ee8 GIT binary patch literal 53 zcmZ?wbhEHbWMp7u_{0DLDk>@-9v->5xf3T&+_r7oty{PL|NpN8k^!kV#-QC?&rc9YLXU>u(OV+Gevt`Scy?gf_J$m%)*|S%#UcGnk-m_=V c-o1PG?c2Bi|NrZNYyjECz{>nUoQc620ARN>tN;K2 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth6.gif b/test/resources/pygif/depth6.gif new file mode 100644 index 0000000000000000000000000000000000000000..0274106bfb91f4b501960b3b59480c539ae1a10e GIT binary patch literal 222 zcmV<403rWJNk%w1VF3UE0QCR>000041Ox~O2n-Ai5D*X)6ciX37#tiNARr(lBqS&( zC@d^2FfcGQG&DFkI6OQ&KtMo5L_|nPNK8yjP*6}+RaIG8SzTRSVPRoqWo2n;X>Dz7 zadB~Vb#-}pd3}9-fq{XAg@uWUiH(hok&%&=m6e&9nVp@Tp`oFrrKPH>s;;iCva+(a zwzj&uy1u@?!otGF#>UFZ%FfQt($dn_*4EnE+TPyY;^N}w=H}|^>hA9D^78Wb_V)Vv Y`v3p`EC2ui00962000I9z+VsmJ7BM4hX4Qo literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth7.gif b/test/resources/pygif/depth7.gif new file mode 100644 index 0000000000000000000000000000000000000000..e31caa20c187264de73ce36099738c6c0fa1f4cd GIT binary patch literal 414 zcmV;P0b%|}Nk%w1VF3UE0QLX?000020s;gC1O^5M2nYxY3JMGi3=R$s5D*X&5)u>? z6c!d17#J8D8X6oN93CDXARr(jA|fOtBqk;%C@3f@Dk>~2EG{lCFfcGOGBPwYG&VLi zI5;>uIyyW&JU%`?KtMo3LPA7DL`FtNNJvOZN=i&jOioTtP*6}(Qc_e@R9042SXfwE zT3TFOTwY#YU|?WkVq#=uWM*b&XlQ6^YHDn3Y;JCDaBy&Pa&mNZbar-jczAevdU||( ze13j@fPjF4f`WvEgocKOh=_=aii(VkjE;_ukdTm)l9H5^l$Ms3n3$NFnwp%PoSvSZ zprD|lqN1dvq^72(sHmu_s;aE4tgf!Eu&}VQva+gw$5?C$RF@bK{R^78cb^!E1l`1ttx`uhC*{Qv*|EC2ui00962000LA IfPaAiJ1I897XSbN literal 0 HcmV?d00001 diff --git a/test/resources/pygif/depth8.gif b/test/resources/pygif/depth8.gif new file mode 100644 index 0000000000000000000000000000000000000000..f2b1496c8d593bc9ae729f2e016adefba18a9798 GIT binary patch literal 799 zcmV+)1K|8eNk%w1VF3UE0QUd@000010RaL60s{jB1Ox;H1qB8M1_uWR2nYxX2?+`c z3JVJh3=9kn4Gj(s4i66x5D*X%5fKs+5)%^>6ciK{6%`g178e&67#J8C85tTH8XFrM z92^`S9UUGX9v>ecARr(iAt53nA|oRsBqSsyB_$>%CMPE+C@3f?DJd!{Dl021EG#T7 zEiEoCE-x=HFfcGNF)=bSGBYzXG&D3dH8nOiHa9mnI5;>tIXOByIy*Z%JUl!-Jv}}? zK0iM{KtMo2K|w-7LPJACL_|bIMMXwNMn^|SNJvOYNl8jdN=r*iOiWBoO-)WtPESuy zP*6}&QBhJ-Qd3h?R8&+|RaI72R##V7SXfwDSy@_IT3cINTwGjTU0q&YUSD5dU|?Wj zVPRroVq;@tWMpJzWo2e&W@l$-XlQ6@X=!R|YHMq2Y;0_8ZEbFDZf|dIaBy&OadC2T za&vQYbaZreb#-=jc6WDoczAeud3kzzdV70&e0+R;eSLm@et&;|fPjF3fq{a8f`fyD zgoK2Jg@uNOhKGlTh=_=ZiHVAeii?YjjEszpjg5|uj*pLzkdTm(k&%*;l9Q8@l$4Z} zm6ev3mY0{8n3$NEnVFiJnwy)OoSdAUot>VZo}ZteprD|kp`oIpqNAguq@<*!rKP5( zrl+T;sHmu^si~@}s;jH3tgNi9t*x%EuCK4Ju&}VPv9YqUva_?Zw6wIfwY9dkwzs#p zxVX5vxw*Q!y1To(yu7@dCU$jHda z$;ryf%FD~k%*@Qq&CSlv&d<-!(9qD)(b3Y<($mw^)YR0~)z#M4*4Nk9*x1lt)=I7_<=;-L_>FMg~>g((4 z?Ck9A?d|UF?(gsK@bK{Q@$vHV^7Hfa^z`)g_4W4l_V@Sq`1ttw`T6?#`uqF){QUg= d{r&#_{{R2~EC2ui00962000OC0RII906VYsg027n literal 0 HcmV?d00001 diff --git a/test/resources/pygif/disabled-transparent.gif b/test/resources/pygif/disabled-transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..dbe0e54d9c9b76a11554776f5ab113147af7be87 GIT binary patch literal 62 zcmZ?wbhEHbWMW`q_{0DL|A7ERfiZ{;!iqmxfP5wf9Uuv$m>3wCnKhgzGFSruWiAmQ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/dispose-keep.gif b/test/resources/pygif/dispose-keep.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e77bb87dcf0a811dbfbe22071dec6974f4f093f GIT binary patch literal 131 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m!eRuZbwEl$iW!)g bc;XPkAT^9Y2o&~VL>2}KVhBUj!i22>+#V5D literal 0 HcmV?d00001 diff --git a/test/resources/pygif/dispose-none.gif b/test/resources/pygif/dispose-none.gif new file mode 100644 index 0000000000000000000000000000000000000000..66c346be8a1df62fe483ae97349ea9be963a2797 GIT binary patch literal 131 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m!e9iXbwEl$iW!)g bc;XPkAT^9Y2o&~VL>2}KVhBUj!i22>*rpLi literal 0 HcmV?d00001 diff --git a/test/resources/pygif/dispose-restore-background.gif b/test/resources/pygif/dispose-restore-background.gif new file mode 100644 index 0000000000000000000000000000000000000000..4ee2622d4d1e6c02f492d38d5282fa37ad6fbf34 GIT binary patch literal 131 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6jl7m!eIoYbwElOfslcT W$%hdk3{rzt7$k_H8ln~^Yz+X{5D`ZJ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/dispose-restore-previous.gif b/test/resources/pygif/dispose-restore-previous.gif new file mode 100644 index 0000000000000000000000000000000000000000..61ac3afcbe2cd4a3dfe5fef9a20d8710a5d3d33c GIT binary patch literal 146 zcmZ?wbhEHbWMW`q_`m=H|NsA2{Lk&@8WQa67~pE8XTZz|6x9JqffO+?F|`CTDE?&O eF@gy*G5}RDG5Ihegh5)c3WEeOR72FlgslP4=n`N6 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/double-clears.gif b/test/resources/pygif/double-clears.gif new file mode 100644 index 0000000000000000000000000000000000000000..dcf54c67c067bc25afcb4080d87c1c62a527790f GIT binary patch literal 148 zcmZ?wbhEHbkp0q literal 0 HcmV?d00001 diff --git a/test/resources/pygif/gif87a-animation.gif b/test/resources/pygif/gif87a-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..900c4e53e7f2a4cb8e2d006fd0e1a2a99a74aa46 GIT binary patch literal 82 zcmZ?wbhEHbWMW`q_`m=H|NsBj0ns241|}w+I0hsxvkL<&GM}j>7@5n=!o{0aU7})Ci%83!1o~i35bFd?d5ixO05|hTrm@G!czI-@xRI#LKltVk58LMk$aQsfG)C={ilQq+p(kd-J$YV0~!bl@Du_n=^8mY-NN|S4}rqGm{N>gi^L%yQ|Q4kj-1!;jS$O=?J zUZ4w#g0i40s0*4yaYQAeBrZux(h^ycm8g=uM3)pLWl2?1mo$e;iYi1!T#;0y6|y3$ zP!)NFt|%(XimIZnXbxo;)rgw7CaFnlWKCA1YVsOgQ`D3-RZU&f9BMRb5Djrd(vUXD zhO9v~IdgtaH)JG_@{0^ z|H8}FDRKSNcYN)VtMwcDt*7t2;L_{L5AF5O-1YU#PSl@;x1Kq9;pMlPV;9|UD!=H8 zllIE{-ad8rC0F0;?s)Zv(?9&iH4pknK7RZ3J(phh@ObK?G< z#N*3{uO2`1lW*Pn#P-?8@0_{!^4p%|yo+ya^|x<7Bd)st-S)mKPQH@d`Pz-o-G9|R zf0B=W^6qmFTz&7K)#;0Gdj6;1dEhVl&HLYb{%6-b_}B8I*KT^@=ihzkgZlGN-h1J} z>mL4_Iq#C2Uo5Ww-QVq154`{4FOEO<4|nJ5H^21Dn|}XK|LCXhzx2?F$Nx2+zVeb2 zFaPS6Km0qq`M?J+|N7P^{@g;xO|S`F zf=lQVe8P}0CQJz-VNO^Q)`Tr#PcSKMicRTKTuPtfQ-+i=Wl9MtbIOvkrfeyDijlMu zE9oRo(o4K#kc^T^5+t)^k*tzUvP(=xn_)A$442Vo_>3WA%$PDl#+A3 zq%E-}U5P8{OMJ;tGL}pwp=2&uO4gFCWG^ulZH2AqDqKZh;VXuUv0|zS6?4T>u~uvq zdxfcKYivzd<7)aEUo+Hmvy*U&fkhM{3>m>NRE+^{sP z4O_$h6)?UmF#eB=vmJ&ZG6+L#NDQe#8Zv`2xS|fx5qBgVX@~5{ zI#fsAp*xC>vZLy#JDLOKB8PCqj>M5Vq$6`ENAA#$!cjUZN9|}1NQ`1H z>rp*Jh?}E3Qy^&Jhi7ez&08X1MxsIkPgU! zY(Nd<1A3qsCwU0M2+MldZZXBN2-x}q&Z+bnh+E5L^6?1 z$cb!1P2>}LqL?Trs)>4{IWRv8h(H`j0%<@7vVaQY0Uam;WuOYwf#x6((TtdhXOfw8 zM$Tk2Y9^o2GsR3fQ_a*f%|T6~1+fq>Bn#<+T*wyGLcX9EiiL8aTBsMAgY-lzVkKTl zR?-!@lC7wfd_}JmE9FYHQm-@zor*TZM!b=1q#JT0+fW<%hTbSP%8hEH-e?Y@7VU_g zcqiFOcjQjCqjvHgy;JOzJJn9T)9hb$7+<=>5S$%njG;AHLuYV?-rx;`VKhvJV3-Yy zVKr=q-C$bU7TeObxR$=fw+t;~%hVED=9Z;pZP{A(7Gr5G*3wy=rMGy?U>PlwC0J(5 zVp%PlWw)4)w!?OG9j>GA@Et?P*fDj4j=5v$SUa|ky~8+Khjnxg=ja{YF*ruY_Y9uVGkJn% z_AH*&vw3!p8E6OWKsVq9`T;*M42%QQKp2<@mVtF(8`uZTNIPOjx)C?hkNA;cWE`1B z!pJK9z!F#kTVM~EnRdp`bTe+IpYb!p%s4a6gqe9}nOSGHnSI7Av`u4icKRK^Gwh5z(@xl#cb1)XXWQAo0>+o@FmC-H Dvrn;m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..db640482a9002113bf37bde2777edf39bc694d3a GIT binary patch literal 68 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixX7J)NC{+@0JF4GfJKbU+G0S{Rs_BsdwY E0RUhV00000 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/icc-color-profile.gif b/test/resources/pygif/icc-color-profile.gif new file mode 100644 index 0000000000000000000000000000000000000000..d0b1126c9641f4f112c599a2f3732558769c05f5 GIT binary patch literal 16822 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 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/image-inside-bg.gif b/test/resources/pygif/image-inside-bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..1820049d2852d9018d4ae636330bf92b8a468983 GIT binary patch literal 53 rcmZ?wbhEHbWMW`q_{7Kn1pk2mM1e7g4Z=Dg8Ac#vU}nj9Pp L$$_~NoD9|ghVKxA literal 0 HcmV?d00001 diff --git a/test/resources/pygif/images-overlap.gif b/test/resources/pygif/images-overlap.gif new file mode 100644 index 0000000000000000000000000000000000000000..6dd1b2a204b273cec2ac14f6eb2aacacb98bb96c GIT binary patch literal 68 tcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!a5)skQxSNCJjy$t^_B8H2|kS4}<^! literal 0 HcmV?d00001 diff --git a/test/resources/pygif/interlace.gif b/test/resources/pygif/interlace.gif new file mode 100644 index 0000000000000000000000000000000000000000..c2116d8b4e2bbdbc668a105271655333286b8fd2 GIT binary patch literal 1087 zcmW;KXz%vBVM2StM{aT{wrXoJ%*(BatMM=}rngNTnxf z^rAQClgAcukEGKj$pVJO4MBcB2aDPlMy7)dds7|j^QGLG>~ zpoED`;vyz9g{fT3G%n#%E@L{Ea|Ks2gPB~#)y(1=uH`yrb3He3BXhWko4JKj$|z?p z^QfSbDyo^!0v57}#nezs9rY|>Da*K(+qj+O+`*mP#ogS)z1+tNR&qZN@F1&L%|oo= zVIJX89^-MI;7Qifz&h5mfsJfpGmSjO(>%koJje6Az!tXhA}{eW+t|)4?BG>i<8|KP zP2S>dcGAQyn%T`eyvuv+;e9^fLq6hTKH*dLvX9UBoG;kV0lwrQU-32H@Gal*JwI@W z!~Dok{LC->%5Pen|IQ!$$zS}4OzjOcUKz!lG`312#b!9=}-75x$$L7?>6|AqwjY!BV3y$cycvSM_9rf`=n-`Bx znYJ^tB9J^ct8GL={;K?SbF)Ju6Sr1`r&JB;m@=(tRl$a;p;2iwGArAsE*##aXifg= z!c7ZDbS+-HwK5{Dc3kgS2b)$GHP((#pM5AMt3Ii`UvOw-(v+r#^8O)V(G$1TC(j$u zE-G#2{-$;Fa>AlBR%R{fUOA{k*W!jLyVh3@j!Z1sw{1yE^{`Iqvk&jzwV^sMI-@iu zdufjah4F)zCrxeMxS%K@Z^gvzOH&t(>^8P>=7Hu-i;5FVwyeyKTh_B?OzQMK4O4e- ht{Iy)WADE0%hKv5^qF(y@PXZpbtM_4M}vZ-9|NhU^~(SN literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-ascii-comment.gif b/test/resources/pygif/invalid-ascii-comment.gif new file mode 100644 index 0000000000000000000000000000000000000000..a0398afcb337acc8f8a5fd58a410e32540f58b85 GIT binary patch literal 59 wcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixWx4)15s0m*|@GB7hqa57i}08UyF!vFvP literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-background.gif b/test/resources/pygif/invalid-background.gif new file mode 100644 index 0000000000000000000000000000000000000000..4943c726a854dbfedf1c42b5c6afb0fc8a3ad6c2 GIT binary patch literal 35 kcmZ?wbhEHbWMp7u`0$?r2>$>7uLGh%A`DDSK8y_30H22kQ2+n{ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-code.gif b/test/resources/pygif/invalid-code.gif new file mode 100644 index 0000000000000000000000000000000000000000..7d929c9431c0c5b7cd53f636f7711d47385f88b2 GIT binary patch literal 35 jcmZ?wbhEHbWMW`q_`m=H|NsBj0ns241}3Ke{~4?Sjj;#^ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-colors.gif b/test/resources/pygif/invalid-colors.gif new file mode 100644 index 0000000000000000000000000000000000000000..c3111525ac2d977a0dbedf917f2beae610b614f8 GIT binary patch literal 37 mcmZ?wbhEHbWMp7u_`t{j1poj4*8$NW5e6QX{|XFD4Auad7zZ-| literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-transparent.gif b/test/resources/pygif/invalid-transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..ce02c1aaa710fd9d9d76b1681685213b44d36057 GIT binary patch literal 62 wcmZ?wbhEHbWMW`q_{0DL|A7ERfiZ{;!iqmx7(rq>AQd3B49v_L&J!7|0eA}%9smFU literal 0 HcmV?d00001 diff --git a/test/resources/pygif/invalid-utf8-comment.gif b/test/resources/pygif/invalid-utf8-comment.gif new file mode 100644 index 0000000000000000000000000000000000000000..893df115324efae4ea28bf9dbda88f83ffefa6d3 GIT binary patch literal 60 xcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixWx4>xNt=ztV}lrk_gNpLb)0{~tB5wHLN literal 0 HcmV?d00001 diff --git a/test/resources/pygif/large-codes.gif b/test/resources/pygif/large-codes.gif new file mode 100644 index 0000000000000000000000000000000000000000..15ae2f4c8efa5ac11eeaccd9e9cf6042c791afff GIT binary patch literal 6358 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 KoAhgc-Tx0F+yN*6 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/large-comment.gif b/test/resources/pygif/large-comment.gif new file mode 100644 index 0000000000000000000000000000000000000000..8991742310d31be0642d668ecb5bc96e331048c5 GIT binary patch literal 13106 zcmeI3I}XAy5JVlQxB+F-a{v%MpA%3cL1!saj!s9z;0+|s9o|^7;*{R?+Vio`*VF!R zZ&H(9DVj=)&)8!ez5D3$%(-uG{g5AB%li>@-qW;2PJRi3h>Lzoe#yPCmH!KZ*xTPA z=C(gqfWT*81VO+ve@=eMy{N$F9|WP%-;jkwy|n_wh5lm@%zxMb!2E{|0L*{b0Koi* z4FJr4Di$QJ1oI!Z0BZij7C`O)umw>2KWqWi{tsIKwg1Dr^RZ++_J7zHKH(=QqT literal 0 HcmV?d00001 diff --git a/test/resources/pygif/max-codes.gif b/test/resources/pygif/max-codes.gif new file mode 100644 index 0000000000000000000000000000000000000000..bd3f8d2da7b83bb14775f9ff7ddf6f0d8deeacf7 GIT binary patch literal 7624 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 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/max-height.gif b/test/resources/pygif/max-height.gif new file mode 100644 index 0000000000000000000000000000000000000000..586d03071bbb0384dfe23646ae4a2cd34b711e6b GIT binary patch literal 405 zcmV;G0c!q7Nk%w1VF3XD|MCC;00030|Ns900093000930|Ns90|Ns90EC2ui00991 z{{RF37`oj4Fv>}*y*TU5yZ>M)j$~<`XsWJk>%MR-&vb3yc&_h!@BhG{a7Zi~kI1BQ z$!t2G(5Q4uty-_xtai)odcWYXcuX#v&*-#z&2GEj@VIs;jK6uCK7Mva__cwzs&s zy1Tr+zQ4f1!o$SH#>dFX%FE2n&d<=%($mz{*4NnC+S}aS-rwNi;^XAy=I7|?>g(+7 z?(gvN^7Hid_V@Vt`uqI-{{H|23LHqVpuvL(6DnNDu%W|;5F<*QNU@^Dix@L%+{m$F zqsNaRLy8oJq5$&6_xL>fFh*r_Y~2g9;r=w5ZXeNRujE z%CxD|r%}*y*TU5yZ>M)j$~<`XsWJk>%MR-&vb3yc&_h!@BhG{a7Zi~kI1BQ z$!t2G(5Q4uty-_xtai)odcWYXcuX#v&*-#z&2GEj@VIs;jK6uCK7Mva__cwzs&s zy1Tr+zQ4f1!o$SH#>dFX%FE2n&d<=%($mz{*4NnC+S}aS-rwNi;^XAy=I7|?>g(+7 z?(gvN^7Hid_V@Vt`uqI-{{H|23LHqVpuvL(6DnNDu%W|;5F<*QNU@^Dix@L%+{m$F zqsNaRLy8oJq5$&6_xL>fFh*r_Y~2g9;r=w5ZXeNRujE z%CxD|r%KM*i6`7knA0|0Kv2T=e3 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/nul-application-extension.gif b/test/resources/pygif/nul-application-extension.gif new file mode 100644 index 0000000000000000000000000000000000000000..176ae31e4ccf06ee0dc8437c0464cdfd229498f5 GIT binary patch literal 78 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixX7L2^*Q0;0hLgARxZ(#*ijB*DpG4FF#Q B5K{mE literal 0 HcmV?d00001 diff --git a/test/resources/pygif/nul-comment.gif b/test/resources/pygif/nul-comment.gif new file mode 100644 index 0000000000000000000000000000000000000000..a0024688f3377ed631084efe1019a6e33e729eab GIT binary patch literal 58 ucmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixVG85ndx(jbKl%uEuT4AuZ5t`I-~ literal 0 HcmV?d00001 diff --git a/test/resources/pygif/plain-text.gif b/test/resources/pygif/plain-text.gif new file mode 100644 index 0000000000000000000000000000000000000000..5be1cb07c3889b4c526da9fa1174b2d6adf2e168 GIT binary patch literal 90 zcmZ?wbhEHb)L`IX_{0DL|A7ERfiZ{;!itPMAUReBMg|TJMg~@o)SR4r1|5(XNGAic ZAjhsd{|rufuHI|$`t0ui3ey=FtO2xe8&CiM literal 0 HcmV?d00001 diff --git a/test/resources/pygif/transparent.gif b/test/resources/pygif/transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..d8be13e4b4d58cf7ca74fc7503fa35cac7b0e659 GIT binary patch literal 62 ycmZ?wbhEHbWMW`q_{0DL|A7ERfiZ{;!iqmx7#V;{bU-RVY8jZBHJm3hSOWlML=hnX literal 0 HcmV?d00001 diff --git a/test/resources/pygif/unknown-application-extension.gif b/test/resources/pygif/unknown-application-extension.gif new file mode 100644 index 0000000000000000000000000000000000000000..9204606dda037b835b101f823aaf245a42040e34 GIT binary patch literal 80 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixX7L;bw{{KNeeBO)SLJyLUW@>#?4i*iyJ QbU>Ow+8LOcBsdwY0Z#50ssI20 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/unknown-extension.gif b/test/resources/pygif/unknown-extension.gif new file mode 100644 index 0000000000000000000000000000000000000000..8518ff6c63ad0f892032edd5469a2a234f275a2e GIT binary patch literal 68 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!irj~9;rDw`K;mjML8)9Iv|B0Eey;|5}XXy E00OcTaR2}S literal 0 HcmV?d00001 diff --git a/test/resources/pygif/xmp-data-empty.gif b/test/resources/pygif/xmp-data-empty.gif new file mode 100644 index 0000000000000000000000000000000000000000..6a7a23d817cf16251214acc3c1d5b93879c696b0 GIT binary patch literal 325 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixX7BYXoCToOwXfi&a)e}Dh{{`K?6_itan zeE#(D!~1t{-@JbH^2PIKPoF$~^zgy`dw1{LzIF4)^=ntJT)uSi!ufM&&zwGW^2G6D zM~@snbnw9beS7!p-nDbb_HA3YY~HkS!}@h=*Q{Q(a>epxOP4HOv~a=vd2{E?o;7pE z^l4M4OrA7xLVsUxPj^>mM|)dqOLJ3WLw#LsO?6deMR{3iNpVqOL4ICtPIgviMtWLm zN^(+SLVR3oOmtLaM0i+eNN`YKfWM!wkGGenhr64ri?frXgT0-tjkT4fg}IrjiLsHP zfxe!uj<%MjhPs-nin5ZTg1nrpjI@-bgt(Zfh_H~L06!lu4>uPl2Rj=p3o{cV1A`7I Pq(EWDz|17U$zTltIFgf4 literal 0 HcmV?d00001 diff --git a/test/resources/pygif/xmp-data.gif b/test/resources/pygif/xmp-data.gif new file mode 100644 index 0000000000000000000000000000000000000000..80535f7af704b1feb4bb940d472e5b022db02d86 GIT binary patch literal 659 zcmZ?wbhEHbWMp7u_{0DL|A7ERfiZ{;!ixX7BYXoCToOwXfwYZ%ML}Y6c4~=2Qfhi; zo~@F-l0s&Rtx~wDuYqrYb81GWM^#a3aFt(3a#eP+Wr~u$9hXgoRYh(=ZfZ%QLPc&) zUa?h$tx{r2ep0FxkPQ;nSF+<$P*AWbN=dT{a&d#I0`hE?GD=Dctn~HE%ggo3jrH=2 z()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij^UTK#g%pElw`VEGWs$&r<*yo0ybeT4JlD z1am=d0o?4oVm+{H^pf*)^(zt!^bPe4pe_PBO2G!`b}Q$i)WnkfqLBRj9J_!@cTYDP zeRN@v4}hxmLAD{;4)GaS6zDZzVCcg`!;Xvb|G&R~e*gOUo>*T>t-)5G1()y3J#(ZSx%*2db((!$)#)Wq1x&_G{LS4Ue*Q$t-% zRYh4zQ9)i#Rz_M%QbJrzR76-vP=KG0mxr5+lY^a&m4%s!k%2)6lj3Dy=|NqtiFNp=x literal 0 HcmV?d00001 diff --git a/test/resources/pygif/zero-width.gif b/test/resources/pygif/zero-width.gif new file mode 100644 index 0000000000000000000000000000000000000000..0c965d4a56b4c91b415965f8528d7ad86fb1f7e5 GIT binary patch literal 20 acmZ?wbhEHbWME)q_`t{j1poj4w*~+&l?BrP literal 0 HcmV?d00001 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); From bb7d3a34b0c057cdc268902a4e71b908a63515ec Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 15:42:38 +1100 Subject: [PATCH 5/7] various minor improvements - extend to php 7.3 - truncate image data if too long for number of pixels (apparently valid) - mix transparency to white --- .travis.yml | 1 + docs/user/formats.rst | 4 +- src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php | 31 ++++++--- .../GfxPhp/Codec/Gif/GifGraphicControlExt.php | 65 +++++++++++++++++-- .../GfxPhp/Codec/Gif/GifGraphicsBlock.php | 8 +++ src/Mike42/GfxPhp/IndexedRasterImage.php | 4 ++ test/integration/GifsuiteTest.php | 1 - 7 files changed, 99 insertions(+), 15 deletions(-) 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/docs/user/formats.rst b/docs/user/formats.rst index 4a0e277..405c4c1 100644 --- a/docs/user/formats.rst +++ b/docs/user/formats.rst @@ -46,9 +46,9 @@ 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 -- Information about transparency is currently not used +- If a transparent color is present, then this will be mixed to white -A GIF image will alwasys be loaded into an isntance of :class:`IndexedRasterImage`, which makes palette information available. +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/Gif/GifDataStream.php b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php index 5da49bd..d7793d8 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -51,7 +51,7 @@ public function toRasterImage(int $imageIndex = 0) : IndexedRasterImage if ($dataBlock -> getGraphicsBlock() !== null && $dataBlock -> getGraphicsBlock() -> getTableBasedImage() != null) { // This is a raster image if ($currentIndex == $imageIndex) { - return GifDataStream::extractImage($this -> logicalScreen, $dataBlock -> getGraphicsBlock() -> getTableBasedImage()); + return GifDataStream::extractImage($this -> logicalScreen, $dataBlock -> getGraphicsBlock() -> getTableBasedImage(), $dataBlock -> getGraphicsBlock() -> getGraphicControlExt()); } $currentIndex++; } @@ -59,11 +59,12 @@ public function toRasterImage(int $imageIndex = 0) : IndexedRasterImage throw new \Exception("Could not find image #$imageIndex in GIF file"); } - private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBasedImage $tableBasedImage) : IndexedRasterImage + 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(); + + $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."); } @@ -71,14 +72,28 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa 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()); + $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)); - return IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); + $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 diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php index 0a29b74..64c60a6 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php @@ -7,14 +7,71 @@ class GifGraphicControlExt { - public function __construct() + private $disposalMethod; + private $hasUserInputFlag; + private $hasTransparentColor; + private $delayTime; + private $transparentColorIndex; + + public function __construct(int $disposalMethod, bool $hasUserInputFlag, bool $hasTransparentColor, int $delayTime, int $transparentColorIndex) + { + $this->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 { - // TODO - $in -> read(8); - return new 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 index 23d28ca..da33d6c 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -48,6 +48,14 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock $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); diff --git a/src/Mike42/GfxPhp/IndexedRasterImage.php b/src/Mike42/GfxPhp/IndexedRasterImage.php index 585d9a7..3599a98 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 index e0834ea..99a0469 100644 --- a/test/integration/GifsuiteTest.php +++ b/test/integration/GifsuiteTest.php @@ -185,7 +185,6 @@ function test_extra_data() { } function test_extra_pixels() { - $this -> markTestSkipped("Known bug: Extra pixels not correctly discarded"); $img = $this -> loadImage("extra-pixels.gif"); $this -> assertEquals(1, $img -> getWidth()); $this -> assertEquals(1, $img -> getHeight()); From 24ee5e6d1c0e362b8e527436e14443c62b135b27 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 15:44:26 +1100 Subject: [PATCH 6/7] phpcs fixes --- src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php | 5 ++--- src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php | 2 +- src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php | 2 +- src/Mike42/GfxPhp/IndexedRasterImage.php | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php index d7793d8..fb26a61 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifDataStream.php @@ -79,8 +79,7 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa $expectedLen = $width * $height; if ($actualLen > $expectedLen) { $decompressedData = substr($decompressedData, 0, $expectedLen); - } else if($actualLen < $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()) { @@ -90,7 +89,7 @@ private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBa $dataArr = array_values(unpack("C*", $decompressedData)); $image = IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette()); // Lastly, transparency handling - if($graphicControlExt != null && $graphicControlExt -> hasTransparentColor()) { + if ($graphicControlExt != null && $graphicControlExt -> hasTransparentColor()) { $image -> setTransparentColor($graphicControlExt -> getTransparentColorIndex()); } return $image; diff --git a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php index 64c60a6..322e735 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicControlExt.php @@ -69,7 +69,7 @@ public static function fromBin(DataInputStream $in) : GifGraphicControlExt $transparentColorIndexData = $in -> read(1); $transparentColorIndex = unpack("C", $transparentColorIndexData)[1]; $end = $in -> read(1); - if($end != "\x00") { + 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 index da33d6c..e2b3b5d 100644 --- a/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php +++ b/src/Mike42/GfxPhp/Codec/Gif/GifGraphicsBlock.php @@ -48,7 +48,7 @@ public static function fromBin(DataInputStream $in) : GifGraphicsBlock $blockId = $peek[0]; $extensionId = $peek[1]; } - if($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_APPLICATION) { + 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); diff --git a/src/Mike42/GfxPhp/IndexedRasterImage.php b/src/Mike42/GfxPhp/IndexedRasterImage.php index 3599a98..43508e2 100644 --- a/src/Mike42/GfxPhp/IndexedRasterImage.php +++ b/src/Mike42/GfxPhp/IndexedRasterImage.php @@ -127,7 +127,7 @@ public function toIndexed(): IndexedRasterImage public function indexToRgb(int $index) { - if($index == $this -> transparentColor) { + if ($index == $this -> transparentColor) { // White return [255, 255, 255]; } From 5967bd84a633966ba662eb93855db50c9fd3969b Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 17 Feb 2019 15:57:39 +1100 Subject: [PATCH 7/7] bump phpcs version for php7.3 compatibility --- composer.json | 6 ++- composer.lock | 139 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 104 insertions(+), 41 deletions(-) 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"