charger une image

This commit is contained in:
2026-01-09 10:51:56 +01:00
parent d0f4ed4eb2
commit e5714b47c6
518 changed files with 40175 additions and 44 deletions

21
upLoadImage/vendor/intervention/gif/LICENSE vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-present Oliver Vogel
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

101
upLoadImage/vendor/intervention/gif/README.md vendored Executable file
View File

@@ -0,0 +1,101 @@
# Intervention GIF
## Native PHP GIF Encoder/Decoder
[![Latest Version](https://img.shields.io/packagist/v/intervention/gif.svg)](https://packagist.org/packages/intervention/gif)
![build](https://github.com/Intervention/gif/actions/workflows/build.yml/badge.svg)
[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/gif.svg)](https://packagist.org/packages/intervention/gif/stats)
[![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/gif/main/.github/images/support.svg)](https://ko-fi.com/interventionphp)
Intervention GIF is a PHP encoder and decoder for the GIF image format that
does not depend on any image processing extension.
Only the special `Splitter::class` class divides the data stream of an animated
GIF into individual `GDImage` objects for each frame and is therefore dependent
on the GD library.
The library is the main component of [Intervention
Image](https://github.com/Intervention/image) for processing animated GIF files
with the GD library, but also works independently.
## Installation
You can easily install this package using [Composer](https://getcomposer.org).
Just request the package with the following command:
```bash
composer require intervention/gif
```
## Code Examples
### Decoding
```php
use Intervention\Gif\Decoder;
// Decode filepath to Intervention\Gif\GifDataStream::class
$gif = Decoder::decode('images/animation.gif');
// Decoder can also handle binary content directly
$gif = Decoder::decode($contents);
```
### Encoding
Use the Builder class to create a new GIF image.
```php
use Intervention\Gif\Builder;
// create new gif canvas
$gif = Builder::canvas(width: 32, height: 32);
// add animation frames to canvas
$delay = .25; // delay in seconds after next frame is displayed
$left = 0; // position offset (left)
$top = 0; // position offset (top)
// add animation frames with optional delay in seconds
// and optional position offset for each frame
$gif->addFrame('images/frame01.gif', $delay, $left, $top);
$gif->addFrame('images/frame02.gif', $delay, $left);
$gif->addFrame('images/frame03.gif', $delay);
$gif->addFrame('images/frame04.gif');
// set loop count; 0 for infinite looping
$gif->setLoops(12);
// encode
$data = $gif->encode();
```
## Requirements
- PHP >= 8.1
## Development & Testing
With this package comes a Docker image to build a test suite and analysis
container. To build this container you have to have Docker installed on your
system. You can run all tests with this command.
```bash
docker-compose run --rm --build tests
```
Run the static analyzer on the code base.
```bash
docker-compose run --rm --build analysis
```
## Authors
This library is developed and maintained by [Oliver Vogel](https://intervention.io)
Thanks to the community of [contributors](https://github.com/Intervention/gif/graphs/contributors) who have helped to improve this project.
## License
Intervention GIF is licensed under the [MIT License](LICENSE).

View File

@@ -0,0 +1,44 @@
{
"name": "intervention/gif",
"description": "Native PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"image",
"gd",
"gif",
"animation"
],
"license": "MIT",
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"require": {
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phpstan/phpstan": "^2.1",
"squizlabs/php_codesniffer": "^3.8",
"slevomat/coding-standard": "~8.0"
},
"autoload": {
"psr-4": {
"Intervention\\Gif\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Intervention\\Gif\\Tests\\": "tests"
}
},
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" beStrictAboutTestsThatDoNotTestAnything="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Unit Tests">
<directory suffix=".php">./tests/Unit/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Traits\CanDecode;
use Intervention\Gif\Traits\CanEncode;
use ReflectionClass;
use Stringable;
abstract class AbstractEntity implements Stringable
{
use CanEncode;
use CanDecode;
public const TERMINATOR = "\x00";
/**
* Get short classname of current instance
*/
public static function getShortClassname(): string
{
return (new ReflectionClass(static::class))->getShortName();
}
/**
* Cast object to string
*
* @throws EncoderException
*/
public function __toString(): string
{
return $this->encode();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
abstract class AbstractExtension extends AbstractEntity
{
public const MARKER = "\x21";
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractExtension;
use Intervention\Gif\Exceptions\RuntimeException;
class ApplicationExtension extends AbstractExtension
{
public const LABEL = "\xFF";
/**
* Application Identifier & Auth Code
*/
protected string $application = '';
/**
* Data Sub Blocks
*
* @var array<DataSubBlock>
*/
protected array $blocks = [];
/**
* Get size of block
*/
public function getBlockSize(): int
{
return strlen($this->application);
}
/**
* Set application name
*/
public function setApplication(string $value): self
{
$this->application = $value;
return $this;
}
/**
* Get application name
*/
public function getApplication(): string
{
return $this->application;
}
/**
* Add block to application extension
*/
public function addBlock(DataSubBlock $block): self
{
$this->blocks[] = $block;
return $this;
}
/**
* Set data sub blocks of instance
*
* @param array<DataSubBlock> $blocks
*/
public function setBlocks(array $blocks): self
{
$this->blocks = $blocks;
return $this;
}
/**
* Get blocks of ApplicationExtension
*
* @return array<DataSubBlock>
*/
public function getBlocks(): array
{
return $this->blocks;
}
/**
* Get first block of ApplicationExtension
*
* @throws RuntimeException
*/
public function getFirstBlock(): DataSubBlock
{
if (!array_key_exists(0, $this->blocks)) {
throw new RuntimeException('Unable to retrieve data sub block.');
}
return $this->blocks[0];
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class Color extends AbstractEntity
{
/**
* Create new instance
*/
public function __construct(
protected int $r = 0,
protected int $g = 0,
protected int $b = 0
) {
//
}
/**
* Get red value
*/
public function getRed(): int
{
return $this->r;
}
/**
* Set red value
*/
public function setRed(int $value): self
{
$this->r = $value;
return $this;
}
/**
* Get green value
*/
public function getGreen(): int
{
return $this->g;
}
/**
* Set green value
*/
public function setGreen(int $value): self
{
$this->g = $value;
return $this;
}
/**
* Get blue value
*/
public function getBlue(): int
{
return $this->b;
}
/**
* Set blue value
*/
public function setBlue(int $value): self
{
$this->b = $value;
return $this;
}
/**
* Return hash value of current color
*/
public function getHash(): string
{
return md5($this->r . $this->g . $this->b);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class ColorTable extends AbstractEntity
{
/**
* Create new instance
*
* @param array<Color> $colors
* @return void
*/
public function __construct(protected array $colors = [])
{
//
}
/**
* Return array of current colors
*
* @return array<Color>
*/
public function getColors(): array
{
return array_values($this->colors);
}
/**
* Add color to table
*/
public function addRgb(int $r, int $g, int $b): self
{
$this->addColor(new Color($r, $g, $b));
return $this;
}
/**
* Add color to table
*/
public function addColor(Color $color): self
{
$this->colors[] = $color;
return $this;
}
/**
* Reset colors to array of color objects
*
* @param array<Color> $colors
*/
public function setColors(array $colors): self
{
$this->empty();
foreach ($colors as $color) {
$this->addColor($color);
}
return $this;
}
/**
* Count colors of current instance
*/
public function countColors(): int
{
return count($this->colors);
}
/**
* Determine if any colors are present on the current table
*/
public function hasColors(): bool
{
return $this->countColors() >= 1;
}
/**
* Empty color table
*/
public function empty(): self
{
$this->colors = [];
return $this;
}
/**
* Get size of color table in logical screen descriptor
*/
public function getLogicalSize(): int
{
return match ($this->countColors()) {
4 => 1,
8 => 2,
16 => 3,
32 => 4,
64 => 5,
128 => 6,
256 => 7,
default => 0,
};
}
/**
* Calculate the number of bytes contained by the current table
*/
public function getByteSize(): int
{
if (!$this->hasColors()) {
return 0;
}
return 3 * pow(2, $this->getLogicalSize() + 1);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractExtension;
class CommentExtension extends AbstractExtension
{
public const LABEL = "\xFE";
/**
* Comment blocks
*
* @var array<string>
*/
protected array $comments = [];
/**
* Get all or one comment
*
* @return array<string>
*/
public function getComments(): array
{
return $this->comments;
}
/**
* Get one comment by key
*/
public function getComment(int $key): mixed
{
return $this->comments[$key] ?? null;
}
/**
* Set comment text
*/
public function addComment(string $value): self
{
$this->comments[] = $value;
return $this;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\FormatException;
class DataSubBlock extends AbstractEntity
{
/**
* Create new instance
*
* @throws FormatException
*/
public function __construct(protected string $value)
{
if ($this->getSize() > 255) {
throw new FormatException(
'Data Sub-Block can not have a block size larger than 255 bytes.'
);
}
}
/**
* Return size of current block
*/
public function getSize(): int
{
return strlen($this->value);
}
/**
* Return block value
*/
public function getValue(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
/**
* The GIF files that can be found on the Internet come in a wide variety
* of forms. Some strictly adhere to the original specification, others do
* not and differ in the actual sequence of blocks or their number.
*
* For this reason, this libary has this (kind of "virtual") FrameBlock,
* which can contain all possible blocks in different order that occur in
* a GIF animation.
*
* - Image Description
* - Local Color Table
* - Image Data Block
* - Plain Text Extension
* - Application Extension
* - Comment Extension
*
* The TableBasedImage block, which is a chain of ImageDescriptor, (Local
* Color Table) and ImageData, is used as a marker for terminating a
* FrameBlock.
*
* So far I have only seen GIF files that follow this scheme. However, there are
* examples which have one (or more) comment extensions added before the end. So
* there can be additional "global comments" that are not part of the FrameBlock
* and are appended to the GifDataStream afterwards.
*/
class FrameBlock extends AbstractEntity
{
protected ?GraphicControlExtension $graphicControlExtension = null;
protected ?ColorTable $colorTable = null;
protected ?PlainTextExtension $plainTextExtension = null;
/**
* @var array<ApplicationExtension> $applicationExtensions
*/
protected array $applicationExtensions = [];
/**
* @var array<CommentExtension> $commentExtensions
*/
protected array $commentExtensions = [];
public function __construct(
protected ImageDescriptor $imageDescriptor = new ImageDescriptor(),
protected ImageData $imageData = new ImageData()
) {
//
}
public function addEntity(AbstractEntity $entity): self
{
return match (true) {
$entity instanceof TableBasedImage => $this->setTableBasedImage($entity),
$entity instanceof GraphicControlExtension => $this->setGraphicControlExtension($entity),
$entity instanceof ImageDescriptor => $this->setImageDescriptor($entity),
$entity instanceof ColorTable => $this->setColorTable($entity),
$entity instanceof ImageData => $this->setImageData($entity),
$entity instanceof PlainTextExtension => $this->setPlainTextExtension($entity),
$entity instanceof NetscapeApplicationExtension,
$entity instanceof ApplicationExtension => $this->addApplicationExtension($entity),
$entity instanceof CommentExtension => $this->addCommentExtension($entity),
default => $this,
};
}
/**
* Return application extensions of current frame block
*
* @return array<ApplicationExtension>
*/
public function getApplicationExtensions(): array
{
return $this->applicationExtensions;
}
/**
* Return comment extensions of current frame block
*
* @return array<CommentExtension>
*/
public function getCommentExtensions(): array
{
return $this->commentExtensions;
}
/**
* Set the graphic control extension
*/
public function setGraphicControlExtension(GraphicControlExtension $extension): self
{
$this->graphicControlExtension = $extension;
return $this;
}
/**
* Get the graphic control extension of the current frame block
*/
public function getGraphicControlExtension(): ?GraphicControlExtension
{
return $this->graphicControlExtension;
}
/**
* Set the image descriptor
*/
public function setImageDescriptor(ImageDescriptor $descriptor): self
{
$this->imageDescriptor = $descriptor;
return $this;
}
/**
* Get the image descriptor of the frame block
*/
public function getImageDescriptor(): ImageDescriptor
{
return $this->imageDescriptor;
}
/**
* Set the color table of the current frame block
*/
public function setColorTable(ColorTable $table): self
{
$this->colorTable = $table;
return $this;
}
/**
* Get color table
*/
public function getColorTable(): ?ColorTable
{
return $this->colorTable;
}
/**
* Determine if frame block has color table
*/
public function hasColorTable(): bool
{
return !is_null($this->colorTable);
}
/**
* Set image data of frame block
*/
public function setImageData(ImageData $data): self
{
$this->imageData = $data;
return $this;
}
/**
* Get image data of current frame block
*/
public function getImageData(): ImageData
{
return $this->imageData;
}
/**
* Set plain text extension
*/
public function setPlainTextExtension(PlainTextExtension $extension): self
{
$this->plainTextExtension = $extension;
return $this;
}
/**
* Get plain text extension
*/
public function getPlainTextExtension(): ?PlainTextExtension
{
return $this->plainTextExtension;
}
/**
* Add given application extension to the current frame block
*/
public function addApplicationExtension(ApplicationExtension $extension): self
{
$this->applicationExtensions[] = $extension;
return $this;
}
/**
* Remove all application extensions from the current frame block.
*/
public function clearApplicationExtensions(): self
{
$this->applicationExtensions = [];
return $this;
}
/**
* Add given comment extension to the current frame block
*/
public function addCommentExtension(CommentExtension $extension): self
{
$this->commentExtensions[] = $extension;
return $this;
}
/**
* Return netscape extension of the frame block if available
*/
public function getNetscapeExtension(): ?NetscapeApplicationExtension
{
$extensions = array_filter(
$this->applicationExtensions,
fn(ApplicationExtension $extension): bool => $extension instanceof NetscapeApplicationExtension,
);
return count($extensions) ? reset($extensions) : null;
}
/**
* Set the table based image of the current frame block
*/
public function setTableBasedImage(TableBasedImage $tableBasedImage): self
{
$this->setImageDescriptor($tableBasedImage->getImageDescriptor());
if ($colorTable = $tableBasedImage->getColorTable()) {
$this->setColorTable($colorTable);
}
$this->setImageData($tableBasedImage->getImageData());
return $this;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractExtension;
use Intervention\Gif\DisposalMethod;
class GraphicControlExtension extends AbstractExtension
{
public const LABEL = "\xF9";
public const BLOCKSIZE = "\x04";
/**
* Existance flag of transparent color
*/
protected bool $transparentColorExistance = false;
/**
* Transparent color index
*/
protected int $transparentColorIndex = 0;
/**
* User input flag
*/
protected bool $userInput = false;
/**
* Create new instance
*/
public function __construct(
protected int $delay = 0,
protected DisposalMethod $disposalMethod = DisposalMethod::UNDEFINED,
) {
//
}
/**
* Set delay time (1/100 second)
*/
public function setDelay(int $value): self
{
$this->delay = $value;
return $this;
}
/**
* Return delay time (1/100 second)
*/
public function getDelay(): int
{
return $this->delay;
}
/**
* Set disposal method
*/
public function setDisposalMethod(DisposalMethod $method): self
{
$this->disposalMethod = $method;
return $this;
}
/**
* Get disposal method
*/
public function getDisposalMethod(): DisposalMethod
{
return $this->disposalMethod;
}
/**
* Get transparent color index
*/
public function getTransparentColorIndex(): int
{
return $this->transparentColorIndex;
}
/**
* Set transparent color index
*/
public function setTransparentColorIndex(int $index): self
{
$this->transparentColorIndex = $index;
return $this;
}
/**
* Get current transparent color existance
*/
public function getTransparentColorExistance(): bool
{
return $this->transparentColorExistance;
}
/**
* Set existance flag of transparent color
*/
public function setTransparentColorExistance(bool $existance = true): self
{
$this->transparentColorExistance = $existance;
return $this;
}
/**
* Get user input flag
*/
public function getUserInput(): bool
{
return $this->userInput;
}
/**
* Set user input flag
*/
public function setUserInput(bool $value = true): self
{
$this->userInput = $value;
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class Header extends AbstractEntity
{
/**
* Header signature
*/
public const SIGNATURE = 'GIF';
/**
* Current GIF version
*/
protected string $version = '89a';
/**
* Set GIF version
*/
public function setVersion(string $value): self
{
$this->version = $value;
return $this;
}
/**
* Return current version
*/
public function getVersion(): string
{
return $this->version;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class ImageData extends AbstractEntity
{
/**
* LZW min. code size
*/
protected int $lzw_min_code_size;
/**
* Sub blocks
*
* @var array<DataSubBlock>
*/
protected array $blocks = [];
/**
* Get LZW min. code size
*/
public function getLzwMinCodeSize(): int
{
return $this->lzw_min_code_size;
}
/**
* Set lzw min. code size
*/
public function setLzwMinCodeSize(int $size): self
{
$this->lzw_min_code_size = $size;
return $this;
}
/**
* Get current data sub blocks
*
* @return array<DataSubBlock>
*/
public function getBlocks(): array
{
return $this->blocks;
}
/**
* Addd sub block
*/
public function addBlock(DataSubBlock $block): self
{
$this->blocks[] = $block;
return $this;
}
/**
* Determine if data sub blocks are present
*/
public function hasBlocks(): bool
{
return count($this->blocks) >= 1;
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class ImageDescriptor extends AbstractEntity
{
public const SEPARATOR = "\x2C";
/**
* Width of frame
*/
protected int $width = 0;
/**
* Height of frame
*/
protected int $height = 0;
/**
* Left position of frame
*/
protected int $left = 0;
/**
* Top position of frame
*/
protected int $top = 0;
/**
* Determine if frame is interlaced
*/
protected bool $interlaced = false;
/**
* Local color table flag
*/
protected bool $localColorTableExistance = false;
/**
* Sort flag of local color table
*/
protected bool $localColorTableSorted = false;
/**
* Size of local color table
*/
protected int $localColorTableSize = 0;
/**
* Get current width
*/
public function getWidth(): int
{
return intval($this->width);
}
/**
* Get current width
*/
public function getHeight(): int
{
return intval($this->height);
}
/**
* Get current Top
*/
public function getTop(): int
{
return intval($this->top);
}
/**
* Get current Left
*/
public function getLeft(): int
{
return intval($this->left);
}
/**
* Set size of current instance
*/
public function setSize(int $width, int $height): self
{
$this->width = $width;
$this->height = $height;
return $this;
}
/**
* Set position of current instance
*/
public function setPosition(int $left, int $top): self
{
$this->left = $left;
$this->top = $top;
return $this;
}
/**
* Determine if frame is interlaced
*/
public function isInterlaced(): bool
{
return $this->interlaced;
}
/**
* Set or unset interlaced value
*/
public function setInterlaced(bool $value = true): self
{
$this->interlaced = $value;
return $this;
}
/**
* Determine if local color table is present
*/
public function getLocalColorTableExistance(): bool
{
return $this->localColorTableExistance;
}
/**
* Alias for getLocalColorTableExistance
*/
public function hasLocalColorTable(): bool
{
return $this->getLocalColorTableExistance();
}
/**
* Set local color table flag
*/
public function setLocalColorTableExistance(bool $existance = true): self
{
$this->localColorTableExistance = $existance;
return $this;
}
/**
* Get local color table sorted flag
*/
public function getLocalColorTableSorted(): bool
{
return $this->localColorTableSorted;
}
/**
* Set local color table sorted flag
*/
public function setLocalColorTableSorted(bool $sorted = true): self
{
$this->localColorTableSorted = $sorted;
return $this;
}
/**
* Get size of local color table
*/
public function getLocalColorTableSize(): int
{
return $this->localColorTableSize;
}
/**
* Get byte size of global color table
*/
public function getLocalColorTableByteSize(): int
{
return 3 * pow(2, $this->getLocalColorTableSize() + 1);
}
/**
* Set size of local color table
*/
public function setLocalColorTableSize(int $size): self
{
$this->localColorTableSize = $size;
return $this;
}
}

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class LogicalScreenDescriptor extends AbstractEntity
{
/**
* Width
*/
protected int $width;
/**
* Height
*/
protected int $height;
/**
* Global color table flag
*/
protected bool $globalColorTableExistance = false;
/**
* Sort flag of global color table
*/
protected bool $globalColorTableSorted = false;
/**
* Size of global color table
*/
protected int $globalColorTableSize = 0;
/**
* Background color index
*/
protected int $backgroundColorIndex = 0;
/**
* Color resolution
*/
protected int $bitsPerPixel = 8;
/**
* Pixel aspect ration
*/
protected int $pixelAspectRatio = 0;
/**
* Set size
*/
public function setSize(int $width, int $height): self
{
$this->width = $width;
$this->height = $height;
return $this;
}
/**
* Get width of current instance
*/
public function getWidth(): int
{
return intval($this->width);
}
/**
* Get height of current instance
*/
public function getHeight(): int
{
return intval($this->height);
}
/**
* Determine if global color table is present
*/
public function getGlobalColorTableExistance(): bool
{
return $this->globalColorTableExistance;
}
/**
* Alias of getGlobalColorTableExistance
*/
public function hasGlobalColorTable(): bool
{
return $this->getGlobalColorTableExistance();
}
/**
* Set global color table flag
*/
public function setGlobalColorTableExistance(bool $existance = true): self
{
$this->globalColorTableExistance = $existance;
return $this;
}
/**
* Get global color table sorted flag
*/
public function getGlobalColorTableSorted(): bool
{
return $this->globalColorTableSorted;
}
/**
* Set global color table sorted flag
*/
public function setGlobalColorTableSorted(bool $sorted = true): self
{
$this->globalColorTableSorted = $sorted;
return $this;
}
/**
* Get size of global color table
*/
public function getGlobalColorTableSize(): int
{
return $this->globalColorTableSize;
}
/**
* Get byte size of global color table
*/
public function getGlobalColorTableByteSize(): int
{
return 3 * pow(2, $this->getGlobalColorTableSize() + 1);
}
/**
* Set size of global color table
*/
public function setGlobalColorTableSize(int $size): self
{
$this->globalColorTableSize = $size;
return $this;
}
/**
* Get background color index
*/
public function getBackgroundColorIndex(): int
{
return $this->backgroundColorIndex;
}
/**
* Set background color index
*/
public function setBackgroundColorIndex(int $index): self
{
$this->backgroundColorIndex = $index;
return $this;
}
/**
* Get current pixel aspect ration
*/
public function getPixelAspectRatio(): int
{
return $this->pixelAspectRatio;
}
/**
* Set pixel aspect ratio
*/
public function setPixelAspectRatio(int $ratio): self
{
$this->pixelAspectRatio = $ratio;
return $this;
}
/**
* Get color resolution
*/
public function getBitsPerPixel(): int
{
return $this->bitsPerPixel;
}
/**
* Set color resolution
*/
public function setBitsPerPixel(int $value): self
{
$this->bitsPerPixel = $value;
return $this;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\Exceptions\FormatException;
use Intervention\Gif\Exceptions\RuntimeException;
class NetscapeApplicationExtension extends ApplicationExtension
{
public const IDENTIFIER = "NETSCAPE";
public const AUTH_CODE = "2.0";
public const SUB_BLOCK_PREFIX = "\x01";
/**
* Create new instance
*
* @throws FormatException
*/
public function __construct()
{
$this->setApplication(self::IDENTIFIER . self::AUTH_CODE);
$this->setBlocks([new DataSubBlock(self::SUB_BLOCK_PREFIX . "\x00\x00")]);
}
/**
* Get number of loops
*
* @throws RuntimeException
*/
public function getLoops(): int
{
$unpacked = unpack('v*', substr($this->getFirstBlock()->getValue(), 1));
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new RuntimeException('Unable to get loop count.');
}
return $unpacked[1];
}
/**
* Set number of loops
*
* @throws FormatException
*/
public function setLoops(int $loops): self
{
$this->setBlocks([
new DataSubBlock(self::SUB_BLOCK_PREFIX . pack('v*', $loops))
]);
return $this;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractExtension;
class PlainTextExtension extends AbstractExtension
{
public const LABEL = "\x01";
/**
* Array of text
*
* @var array<string>
*/
protected array $text = [];
/**
* Get current text
*
* @return array<string>
*/
public function getText(): array
{
return $this->text;
}
/**
* Add text
*/
public function addText(string $text): self
{
$this->text[] = $text;
return $this;
}
/**
* Set text array of extension
*
* @param array<string> $text
*/
public function setText(array $text): self
{
$this->text = $text;
return $this;
}
/**
* Determine if any text is present
*/
public function hasText(): bool
{
return $this->text !== [];
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class TableBasedImage extends AbstractEntity
{
protected ImageDescriptor $imageDescriptor;
protected ?ColorTable $colorTable = null;
protected ImageData $imageData;
public function getImageDescriptor(): ImageDescriptor
{
return $this->imageDescriptor;
}
public function setImageDescriptor(ImageDescriptor $descriptor): self
{
$this->imageDescriptor = $descriptor;
return $this;
}
public function getImageData(): ImageData
{
return $this->imageData;
}
public function setImageData(ImageData $data): self
{
$this->imageData = $data;
return $this;
}
public function getColorTable(): ?ColorTable
{
return $this->colorTable;
}
public function setColorTable(ColorTable $table): self
{
$this->colorTable = $table;
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
class Trailer extends AbstractEntity
{
public const MARKER = "\x3b";
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use Exception;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Blocks\GraphicControlExtension;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Blocks\TableBasedImage;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Traits\CanHandleFiles;
class Builder
{
use CanHandleFiles;
/**
* Create new instance
*/
public function __construct(protected GifDataStream $gif = new GifDataStream())
{
//
}
/**
* Create new canvas
*/
public static function canvas(int $width, int $height): self
{
return (new self())->setSize($width, $height);
}
/**
* Get GifDataStream object we're currently building
*/
public function getGifDataStream(): GifDataStream
{
return $this->gif;
}
/**
* Set canvas size of gif
*/
public function setSize(int $width, int $height): self
{
$this->gif->getLogicalScreenDescriptor()->setSize($width, $height);
return $this;
}
/**
* Set loop count
*
* @throws Exception
*/
public function setLoops(int $loops): self
{
if ($loops < 0) {
throw new Exception('The loop count must be equal to or greater than 0');
}
if ($this->gif->getFrames() === []) {
throw new Exception('Add at least one frame before setting the loop count');
}
// with one single loop the netscape extension must be removed otherwise the
// gif is looped twice because the first repetition always takes place
if ($loops === 1) {
$this->gif->getFirstFrame()?->clearApplicationExtensions();
return $this;
}
// make sure a netscape extension is present to store the loop count
if (!$this->gif->getFirstFrame()?->getNetscapeExtension()) {
$this->gif->getFirstFrame()?->addApplicationExtension(
new NetscapeApplicationExtension()
);
}
// the loop count is reduced by one because what is referred to here as
// the “loop count” actually means repetitions in GIF format, and thus
// the first repetition always takes place. A loop count of 0 howerver
// means infinite repetitions and remains unaltered.
$loops = $loops === 0 ? $loops : $loops - 1;
// add loop count to netscape extension on first frame
$this->gif->getFirstFrame()?->getNetscapeExtension()?->setLoops($loops);
return $this;
}
/**
* Create new animation frame from given source
* which can be path to a file or GIF image data
*
* @throws DecoderException
*/
public function addFrame(
mixed $source,
float $delay = 0,
int $left = 0,
int $top = 0,
bool $interlaced = false
): self {
$frame = new FrameBlock();
$source = Decoder::decode($source);
// store delay
$frame->setGraphicControlExtension(
$this->buildGraphicControlExtension(
$source,
intval($delay * 100)
)
);
// store image
$frame->setTableBasedImage(
$this->buildTableBasedImage($source, $left, $top, $interlaced)
);
// add frame
$this->gif->addFrame($frame);
return $this;
}
/**
* Build new graphic control extension with given delay & disposal method
*/
protected function buildGraphicControlExtension(
GifDataStream $source,
int $delay,
DisposalMethod $disposalMethod = DisposalMethod::BACKGROUND
): GraphicControlExtension {
// create extension
$extension = new GraphicControlExtension($delay, $disposalMethod);
// set transparency index
$control = $source->getFirstFrame()->getGraphicControlExtension();
if ($control && $control->getTransparentColorExistance()) {
$extension->setTransparentColorExistance();
$extension->setTransparentColorIndex(
$control->getTransparentColorIndex()
);
}
return $extension;
}
/**
* Build table based image object from given source
*/
protected function buildTableBasedImage(
GifDataStream $source,
int $left,
int $top,
bool $interlaced
): TableBasedImage {
$block = new TableBasedImage();
$block->setImageDescriptor(new ImageDescriptor());
// set global color table from source as local color table
$block->getImageDescriptor()->setLocalColorTableExistance();
$block->setColorTable($source->getGlobalColorTable());
$block->getImageDescriptor()->setLocalColorTableSorted(
$source->getLogicalScreenDescriptor()->getGlobalColorTableSorted()
);
$block->getImageDescriptor()->setLocalColorTableSize(
$source->getLogicalScreenDescriptor()->getGlobalColorTableSize()
);
$block->getImageDescriptor()->setSize(
$source->getLogicalScreenDescriptor()->getWidth(),
$source->getLogicalScreenDescriptor()->getHeight()
);
// set position
$block->getImageDescriptor()->setPosition($left, $top);
// set interlaced flag
$block->getImageDescriptor()->setInterlaced($interlaced);
// add image data from source
$block->setImageData($source->getFirstFrame()->getImageData());
return $block;
}
/**
* Encode the current build
*
* @throws EncoderException
*/
public function encode(): string
{
return $this->gif->encode();
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\RuntimeException;
use Intervention\Gif\Traits\CanHandleFiles;
class Decoder
{
use CanHandleFiles;
/**
* Decode given input
*
* @throws DecoderException
*/
public static function decode(mixed $input): GifDataStream
{
try {
$handle = match (true) {
self::isFilePath($input) => self::getHandleFromFilePath($input),
is_string($input) => self::getHandleFromData($input),
self::isFileHandle($input) => $input,
default => throw new DecoderException(
'Decoder input must be either file path, file pointer resource or binary data.'
)
};
} catch (RuntimeException $e) {
throw new DecoderException($e->getMessage());
}
rewind($handle);
return GifDataStream::decode($handle);
}
/**
* Determine if input is file pointer resource
*/
private static function isFileHandle(mixed $input): bool
{
return is_resource($input) && get_resource_type($input) === 'stream';
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Exceptions\DecoderException;
abstract class AbstractDecoder
{
/**
* Decode current source
*/
abstract public function decode(): mixed;
/**
* Create new instance
*/
public function __construct(protected mixed $handle, protected ?int $length = null)
{
//
}
/**
* Set source to decode
*/
public function setHandle(mixed $handle): self
{
$this->handle = $handle;
return $this;
}
/**
* Read given number of bytes and move file pointer
*
* @throws DecoderException
*/
protected function getNextBytesOrFail(int $length): string
{
if ($length < 1) {
throw new DecoderException('The length passed must be at least one byte.');
}
$bytes = fread($this->handle, $length);
if ($bytes === false || strlen($bytes) !== $length) {
throw new DecoderException('Unexpected end of file.');
}
return $bytes;
}
/**
* Read given number of bytes and move pointer back to previous position
*
* @throws DecoderException
*/
protected function viewNextBytesOrFail(int $length): string
{
$bytes = $this->getNextBytesOrFail($length);
$this->movePointer($length * -1);
return $bytes;
}
/**
* Read next byte and move pointer back to previous position
*
* @throws DecoderException
*/
protected function viewNextByteOrFail(): string
{
return $this->viewNextBytesOrFail(1);
}
/**
* Read all remaining bytes from file handler
*/
protected function getRemainingBytes(): string
{
$all = '';
do {
$byte = fread($this->handle, 1);
$all .= $byte;
} while (!feof($this->handle));
return $all;
}
/**
* Get next byte in stream and move file pointer
*
* @throws DecoderException
*/
protected function getNextByteOrFail(): string
{
return $this->getNextBytesOrFail(1);
}
/**
* Move file pointer on handle by given offset
*/
protected function movePointer(int $offset): self
{
fseek($this->handle, $offset, SEEK_CUR);
return $this;
}
/**
* Decode multi byte value
*
* @throws DecoderException
*/
protected function decodeMultiByte(string $bytes): int
{
$unpacked = unpack('v*', $bytes);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode given bytes.');
}
return $unpacked[1];
}
/**
* Set length
*/
public function setLength(int $length): self
{
$this->length = $length;
return $this;
}
/**
* Get length
*/
public function getLength(): ?int
{
return $this->length;
}
/**
* Get current handle position
*
* @throws DecoderException
*/
public function getPosition(): int
{
$position = ftell($this->handle);
if ($position === false) {
throw new DecoderException('Unable to read current position from handle.');
}
return $position;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Exceptions\DecoderException;
abstract class AbstractPackedBitDecoder extends AbstractDecoder
{
/**
* Decode packed byte
*
* @throws DecoderException
*/
protected function decodePackedByte(string $byte): int
{
$unpacked = unpack('C', $byte);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to get info block size.');
}
return intval($unpacked[1]);
}
/**
* Determine if packed bit is set
*
* @throws DecoderException
*/
protected function hasPackedBit(string $byte, int $num): bool
{
return (bool) $this->getPackedBits($byte)[$num];
}
/**
* Get packed bits
*
* @throws DecoderException
*/
protected function getPackedBits(string $byte, int $start = 0, int $length = 8): string
{
$bits = str_pad(decbin($this->decodePackedByte($byte)), 8, '0', STR_PAD_LEFT);
return substr($bits, $start, $length);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\FormatException;
class ApplicationExtensionDecoder extends AbstractDecoder
{
/**
* Decode current source
*
* @throws FormatException
* @throws DecoderException
*/
public function decode(): ApplicationExtension
{
$result = new ApplicationExtension();
$this->getNextByteOrFail(); // marker
$this->getNextByteOrFail(); // label
$blocksize = $this->decodeBlockSize($this->getNextByteOrFail());
$application = $this->getNextBytesOrFail($blocksize);
if ($application === NetscapeApplicationExtension::IDENTIFIER . NetscapeApplicationExtension::AUTH_CODE) {
$result = new NetscapeApplicationExtension();
// skip length
$this->getNextByteOrFail();
$result->setBlocks([
new DataSubBlock(
$this->getNextBytesOrFail(3)
)
]);
// skip terminator
$this->getNextByteOrFail();
return $result;
}
$result->setApplication($application);
// decode data sub blocks
$blocksize = $this->decodeBlockSize($this->getNextByteOrFail());
while ($blocksize > 0) {
$result->addBlock(new DataSubBlock($this->getNextBytesOrFail($blocksize)));
$blocksize = $this->decodeBlockSize($this->getNextByteOrFail());
}
return $result;
}
/**
* Decode block size of ApplicationExtension from given byte
*
* @throws DecoderException
*/
protected function decodeBlockSize(string $byte): int
{
$unpacked = @unpack('C', $byte);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode application extension block size.');
}
return intval($unpacked[1]);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\Color;
use Intervention\Gif\Exceptions\DecoderException;
class ColorDecoder extends AbstractDecoder
{
/**
* Decode current source to Color
*
* @throws DecoderException
*/
public function decode(): Color
{
$color = new Color();
$color->setRed($this->decodeColorValue($this->getNextByteOrFail()));
$color->setGreen($this->decodeColorValue($this->getNextByteOrFail()));
$color->setBlue($this->decodeColorValue($this->getNextByteOrFail()));
return $color;
}
/**
* Decode red value from source
*
* @throws DecoderException
*/
protected function decodeColorValue(string $byte): int
{
$unpacked = unpack('C', $byte);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode color value.');
}
return $unpacked[1];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\Color;
use Intervention\Gif\Blocks\ColorTable;
use Intervention\Gif\Exceptions\DecoderException;
class ColorTableDecoder extends AbstractDecoder
{
/**
* Decode given string to ColorTable
*
* @throws DecoderException
*/
public function decode(): ColorTable
{
$table = new ColorTable();
for ($i = 0; $i < ($this->getLength() / 3); $i++) {
$table->addColor(Color::decode($this->handle));
}
return $table;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Exceptions\DecoderException;
class CommentExtensionDecoder extends AbstractDecoder
{
/**
* Decode current source
*
* @throws DecoderException
*/
public function decode(): CommentExtension
{
$this->getNextBytesOrFail(2); // skip marker & label
$extension = new CommentExtension();
foreach ($this->decodeComments() as $comment) {
$extension->addComment($comment);
}
return $extension;
}
/**
* Decode comment from current source
*
* @throws DecoderException
* @return array<string>
*/
protected function decodeComments(): array
{
$comments = [];
do {
$byte = $this->getNextByteOrFail();
$size = $this->decodeBlocksize($byte);
if ($size > 0) {
$comments[] = $this->getNextBytesOrFail($size);
}
} while ($byte !== CommentExtension::TERMINATOR);
return $comments;
}
/**
* Decode blocksize of following comment
*
* @throws DecoderException
*/
protected function decodeBlocksize(string $byte): int
{
$unpacked = @unpack('C', $byte);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode comment extension block size.');
}
return intval($unpacked[1]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\FormatException;
class DataSubBlockDecoder extends AbstractDecoder
{
/**
* Decode current sourc
*
* @throws FormatException
* @throws DecoderException
*/
public function decode(): DataSubBlock
{
$char = $this->getNextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode data sub block.');
}
$size = (int) $unpacked[1];
return new DataSubBlock($this->getNextBytesOrFail($size));
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\AbstractExtension;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Blocks\GraphicControlExtension;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Blocks\PlainTextExtension;
use Intervention\Gif\Blocks\TableBasedImage;
use Intervention\Gif\Exceptions\DecoderException;
class FrameBlockDecoder extends AbstractDecoder
{
/**
* Decode FrameBlock
*
* @throws DecoderException
*/
public function decode(): FrameBlock
{
$frame = new FrameBlock();
do {
$block = match ($this->viewNextBytesOrFail(2)) {
AbstractExtension::MARKER . GraphicControlExtension::LABEL
=> GraphicControlExtension::decode($this->handle),
AbstractExtension::MARKER . NetscapeApplicationExtension::LABEL
=> NetscapeApplicationExtension::decode($this->handle),
AbstractExtension::MARKER . ApplicationExtension::LABEL
=> ApplicationExtension::decode($this->handle),
AbstractExtension::MARKER . PlainTextExtension::LABEL
=> PlainTextExtension::decode($this->handle),
AbstractExtension::MARKER . CommentExtension::LABEL
=> CommentExtension::decode($this->handle),
default => match ($this->viewNextByteOrFail()) {
ImageDescriptor::SEPARATOR => TableBasedImage::decode($this->handle),
default => throw new DecoderException('Unable to decode Data Block'),
}
};
$frame->addEntity($block);
} while (!($block instanceof TableBasedImage));
return $frame;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\AbstractExtension;
use Intervention\Gif\Blocks\ColorTable;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Blocks\Header;
use Intervention\Gif\Blocks\LogicalScreenDescriptor;
use Intervention\Gif\Blocks\Trailer;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\GifDataStream;
class GifDataStreamDecoder extends AbstractDecoder
{
/**
* Decode current source to GifDataStream
*
* @throws DecoderException
*/
public function decode(): GifDataStream
{
$gif = new GifDataStream();
$gif->setHeader(
Header::decode($this->handle),
);
$gif->setLogicalScreenDescriptor(
LogicalScreenDescriptor::decode($this->handle),
);
if ($gif->getLogicalScreenDescriptor()->hasGlobalColorTable()) {
$length = $gif->getLogicalScreenDescriptor()->getGlobalColorTableByteSize();
$gif->setGlobalColorTable(
ColorTable::decode($this->handle, $length)
);
}
while ($this->viewNextByteOrFail() !== Trailer::MARKER) {
match ($this->viewNextBytesOrFail(2)) {
// trailing "global" comment blocks which are not part of "FrameBlock"
AbstractExtension::MARKER . CommentExtension::LABEL
=> $gif->addComment(
CommentExtension::decode($this->handle)
),
default => $gif->addFrame(
FrameBlock::decode($this->handle)
),
};
}
return $gif;
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\GraphicControlExtension;
use Intervention\Gif\DisposalMethod;
use Intervention\Gif\Exceptions\DecoderException;
class GraphicControlExtensionDecoder extends AbstractPackedBitDecoder
{
/**
* Decode given string to current instance
*
* @throws DecoderException
*/
public function decode(): GraphicControlExtension
{
$result = new GraphicControlExtension();
// bytes 1-3
$this->getNextBytesOrFail(3); // skip marker, label & bytesize
// byte #4
$packedField = $this->getNextByteOrFail();
$result->setDisposalMethod($this->decodeDisposalMethod($packedField));
$result->setUserInput($this->decodeUserInput($packedField));
$result->setTransparentColorExistance($this->decodeTransparentColorExistance($packedField));
// bytes 5-6
$result->setDelay($this->decodeDelay($this->getNextBytesOrFail(2)));
// byte #7
$result->setTransparentColorIndex($this->decodeTransparentColorIndex(
$this->getNextByteOrFail()
));
// byte #8 (terminator)
$this->getNextByteOrFail();
return $result;
}
/**
* Decode disposal method
*
* @throws DecoderException
*/
protected function decodeDisposalMethod(string $byte): DisposalMethod
{
return DisposalMethod::from(
intval(bindec($this->getPackedBits($byte, 3, 3)))
);
}
/**
* Decode user input flag
*
* @throws DecoderException
*/
protected function decodeUserInput(string $byte): bool
{
return $this->hasPackedBit($byte, 6);
}
/**
* Decode transparent color existance
*
* @throws DecoderException
*/
protected function decodeTransparentColorExistance(string $byte): bool
{
return $this->hasPackedBit($byte, 7);
}
/**
* Decode delay value
*
* @throws DecoderException
*/
protected function decodeDelay(string $bytes): int
{
$unpacked = unpack('v*', $bytes);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode animation delay.');
}
return $unpacked[1];
}
/**
* Decode transparent color index
*
* @throws DecoderException
*/
protected function decodeTransparentColorIndex(string $byte): int
{
$unpacked = unpack('C', $byte);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode transparent color index.');
}
return $unpacked[1];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Blocks\Header;
class HeaderDecoder extends AbstractDecoder
{
/**
* Decode current sourc
*
* @throws DecoderException
*/
public function decode(): Header
{
$header = new Header();
$header->setVersion($this->decodeVersion());
return $header;
}
/**
* Decode version string
*
* @throws DecoderException
*/
protected function decodeVersion(): string
{
$parsed = (bool) preg_match("/^GIF(?P<version>[0-9]{2}[a-z])$/", $this->getNextBytesOrFail(6), $matches);
if ($parsed === false) {
throw new DecoderException('Unable to parse file header.');
}
return $matches['version'];
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Blocks\ImageData;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\FormatException;
class ImageDataDecoder extends AbstractDecoder
{
/**
* Decode current source
*
* @throws DecoderException
* @throws FormatException
*/
public function decode(): ImageData
{
$data = new ImageData();
// LZW min. code size
$char = $this->getNextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode lzw min. code size.');
}
$data->setLzwMinCodeSize(intval($unpacked[1]));
do {
// decode sub blocks
$char = $this->getNextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode image data sub block.');
}
$size = intval($unpacked[1]);
if ($size > 0) {
$data->addBlock(new DataSubBlock($this->getNextBytesOrFail($size)));
}
} while ($char !== AbstractEntity::TERMINATOR);
return $data;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Exceptions\DecoderException;
class ImageDescriptorDecoder extends AbstractPackedBitDecoder
{
/**
* Decode given string to current instance
*
* @throws DecoderException
*/
public function decode(): ImageDescriptor
{
$descriptor = new ImageDescriptor();
$this->getNextByteOrFail(); // skip separator
$descriptor->setPosition(
$this->decodeMultiByte($this->getNextBytesOrFail(2)),
$this->decodeMultiByte($this->getNextBytesOrFail(2))
);
$descriptor->setSize(
$this->decodeMultiByte($this->getNextBytesOrFail(2)),
$this->decodeMultiByte($this->getNextBytesOrFail(2))
);
$packedField = $this->getNextByteOrFail();
$descriptor->setLocalColorTableExistance(
$this->decodeLocalColorTableExistance($packedField)
);
$descriptor->setLocalColorTableSorted(
$this->decodeLocalColorTableSorted($packedField)
);
$descriptor->setLocalColorTableSize(
$this->decodeLocalColorTableSize($packedField)
);
$descriptor->setInterlaced(
$this->decodeInterlaced($packedField)
);
return $descriptor;
}
/**
* Decode local color table existance
*
* @throws DecoderException
*/
protected function decodeLocalColorTableExistance(string $byte): bool
{
return $this->hasPackedBit($byte, 0);
}
/**
* Decode local color table sort method
*
* @throws DecoderException
*/
protected function decodeLocalColorTableSorted(string $byte): bool
{
return $this->hasPackedBit($byte, 2);
}
/**
* Decode local color table size
*
* @throws DecoderException
*/
protected function decodeLocalColorTableSize(string $byte): int
{
return (int) bindec($this->getPackedBits($byte, 5, 3));
}
/**
* Decode interlaced flag
*
* @throws DecoderException
*/
protected function decodeInterlaced(string $byte): bool
{
return $this->hasPackedBit($byte, 1);
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\LogicalScreenDescriptor;
use Intervention\Gif\Exceptions\DecoderException;
class LogicalScreenDescriptorDecoder extends AbstractPackedBitDecoder
{
/**
* Decode given string to current instance
*
* @throws DecoderException
*/
public function decode(): LogicalScreenDescriptor
{
$logicalScreenDescriptor = new LogicalScreenDescriptor();
// bytes 1-4
$logicalScreenDescriptor->setSize(
$this->decodeWidth($this->getNextBytesOrFail(2)),
$this->decodeHeight($this->getNextBytesOrFail(2))
);
// byte 5
$packedField = $this->getNextByteOrFail();
$logicalScreenDescriptor->setGlobalColorTableExistance(
$this->decodeGlobalColorTableExistance($packedField)
);
$logicalScreenDescriptor->setBitsPerPixel(
$this->decodeBitsPerPixel($packedField)
);
$logicalScreenDescriptor->setGlobalColorTableSorted(
$this->decodeGlobalColorTableSorted($packedField)
);
$logicalScreenDescriptor->setGlobalColorTableSize(
$this->decodeGlobalColorTableSize($packedField)
);
// byte 6
$logicalScreenDescriptor->setBackgroundColorIndex(
$this->decodeBackgroundColorIndex($this->getNextByteOrFail())
);
// byte 7
$logicalScreenDescriptor->setPixelAspectRatio(
$this->decodePixelAspectRatio($this->getNextByteOrFail())
);
return $logicalScreenDescriptor;
}
/**
* Decode width
*
* @throws DecoderException
*/
protected function decodeWidth(string $source): int
{
$unpacked = unpack('v*', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode width.');
}
return $unpacked[1];
}
/**
* Decode height
*
* @throws DecoderException
*/
protected function decodeHeight(string $source): int
{
$unpacked = unpack('v*', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode height.');
}
return $unpacked[1];
}
/**
* Decode existance of global color table
*
* @throws DecoderException
*/
protected function decodeGlobalColorTableExistance(string $byte): bool
{
return $this->hasPackedBit($byte, 0);
}
/**
* Decode color resolution in bits per pixel
*
* @throws DecoderException
*/
protected function decodeBitsPerPixel(string $byte): int
{
return intval(bindec($this->getPackedBits($byte, 1, 3))) + 1;
}
/**
* Decode global color table sorted status
*
* @throws DecoderException
*/
protected function decodeGlobalColorTableSorted(string $byte): bool
{
return $this->hasPackedBit($byte, 4);
}
/**
* Decode size of global color table
*
* @throws DecoderException
*/
protected function decodeGlobalColorTableSize(string $byte): int
{
return intval(bindec($this->getPackedBits($byte, 5, 3)));
}
/**
* Decode background color index
*
* @throws DecoderException
*/
protected function decodeBackgroundColorIndex(string $source): int
{
$unpacked = unpack('C', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode background color index.');
}
return $unpacked[1];
}
/**
* Decode pixel aspect ratio
*
* @throws DecoderException
*/
protected function decodePixelAspectRatio(string $source): int
{
$unpacked = unpack('C', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode pixel aspect ratio.');
}
return $unpacked[1];
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
class NetscapeApplicationExtensionDecoder extends ApplicationExtensionDecoder
{
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\PlainTextExtension;
use Intervention\Gif\Exceptions\DecoderException;
class PlainTextExtensionDecoder extends AbstractDecoder
{
/**
* Decode current source
*
* @throws DecoderException
*/
public function decode(): PlainTextExtension
{
$extension = new PlainTextExtension();
// skip marker & label
$this->getNextBytesOrFail(2);
// skip info block
$this->getNextBytesOrFail($this->getInfoBlockSize());
// text blocks
$extension->setText($this->decodeTextBlocks());
return $extension;
}
/**
* Get number of bytes in header block
*
* @throws DecoderException
*/
protected function getInfoBlockSize(): int
{
$unpacked = unpack('C', $this->getNextByteOrFail());
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode info block size.');
}
return $unpacked[1];
}
/**
* Decode text sub blocks
*
* @throws DecoderException
* @return array<string>
*/
protected function decodeTextBlocks(): array
{
$blocks = [];
do {
$char = $this->getNextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Unable to decode text block.');
}
$size = (int) $unpacked[1];
if ($size > 0) {
$blocks[] = $this->getNextBytesOrFail($size);
}
} while ($char !== PlainTextExtension::TERMINATOR);
return $blocks;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\ColorTable;
use Intervention\Gif\Blocks\ImageData;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Blocks\TableBasedImage;
use Intervention\Gif\Exceptions\DecoderException;
class TableBasedImageDecoder extends AbstractDecoder
{
/**
* Decode TableBasedImage
*
* @throws DecoderException
*/
public function decode(): TableBasedImage
{
$block = new TableBasedImage();
$block->setImageDescriptor(ImageDescriptor::decode($this->handle));
if ($block->getImageDescriptor()->hasLocalColorTable()) {
$block->setColorTable(
ColorTable::decode(
$this->handle,
$block->getImageDescriptor()->getLocalColorTableByteSize()
)
);
}
$block->setImageData(
ImageData::decode($this->handle)
);
return $block;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
enum DisposalMethod: int
{
case UNDEFINED = 0;
case NONE = 1; // overlay each frame in sequence
case BACKGROUND = 2; // clear to background (as indicated by the logical screen descriptor)
case PREVIOUS = 3; // restore the canvas to its previous state
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
abstract class AbstractEncoder
{
/**
* Encode current source
*/
abstract public function encode(): string;
/**
* Create new instance
*/
public function __construct(protected mixed $source)
{
//
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Exceptions\EncoderException;
class ApplicationExtensionEncoder extends AbstractEncoder
{
/**
* Create new decoder instance
*/
public function __construct(ApplicationExtension $source)
{
$this->source = $source;
}
/**
* Encode current source
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', [
ApplicationExtension::MARKER,
ApplicationExtension::LABEL,
pack('C', $this->source->getBlockSize()),
$this->source->getApplication(),
implode('', array_map(fn(DataSubBlock $block): string => $block->encode(), $this->source->getBlocks())),
ApplicationExtension::TERMINATOR,
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\Color;
class ColorEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(Color $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
$this->encodeColorValue($this->source->getRed()),
$this->encodeColorValue($this->source->getGreen()),
$this->encodeColorValue($this->source->getBlue()),
]);
}
/**
* Encode color value
*/
protected function encodeColorValue(int $value): string
{
return pack('C', $value);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\Color;
use Intervention\Gif\Blocks\ColorTable;
use Intervention\Gif\Exceptions\EncoderException;
class ColorTableEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(ColorTable $source)
{
$this->source = $source;
}
/**
* Encode current source
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', array_map(
fn(Color $color): string => $color->encode(),
$this->source->getColors(),
));
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\CommentExtension;
class CommentExtensionEncoder extends AbstractEncoder
{
/**
* Create new decoder instance
*/
public function __construct(CommentExtension $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
CommentExtension::MARKER,
CommentExtension::LABEL,
$this->encodeComments(),
CommentExtension::TERMINATOR,
]);
}
/**
* Encode comment blocks
*/
protected function encodeComments(): string
{
return implode('', array_map(function (string $comment): string {
return pack('C', strlen($comment)) . $comment;
}, $this->source->getComments()));
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\DataSubBlock;
class DataSubBlockEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(DataSubBlock $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return pack('C', $this->source->getSize()) . $this->source->getValue();
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Exceptions\EncoderException;
class FrameBlockEncoder extends AbstractEncoder
{
/**
* Create new decoder instance
*/
public function __construct(FrameBlock $source)
{
$this->source = $source;
}
/**
* @throws EncoderException
*/
public function encode(): string
{
$graphicControlExtension = $this->source->getGraphicControlExtension();
$colorTable = $this->source->getColorTable();
$plainTextExtension = $this->source->getPlainTextExtension();
return implode('', [
implode('', array_map(
fn(ApplicationExtension $extension): string => $extension->encode(),
$this->source->getApplicationExtensions(),
)),
implode('', array_map(
fn(CommentExtension $extension): string => $extension->encode(),
$this->source->getCommentExtensions(),
)),
$plainTextExtension ? $plainTextExtension->encode() : '',
$graphicControlExtension ? $graphicControlExtension->encode() : '',
$this->source->getImageDescriptor()->encode(),
$colorTable ? $colorTable->encode() : '',
$this->source->getImageData()->encode(),
]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\GifDataStream;
class GifDataStreamEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(GifDataStream $source)
{
$this->source = $source;
}
/**
* Encode current source
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', [
$this->source->getHeader()->encode(),
$this->source->getLogicalScreenDescriptor()->encode(),
$this->maybeEncodeGlobalColorTable(),
$this->encodeFrames(),
$this->encodeComments(),
$this->source->getTrailer()->encode(),
]);
}
protected function maybeEncodeGlobalColorTable(): string
{
if (!$this->source->hasGlobalColorTable()) {
return '';
}
return $this->source->getGlobalColorTable()->encode();
}
/**
* Encode data blocks of source
*
* @throws EncoderException
*/
protected function encodeFrames(): string
{
return implode('', array_map(
fn(FrameBlock $frame): string => $frame->encode(),
$this->source->getFrames(),
));
}
/**
* Encode comment extension blocks of source
*
* @throws EncoderException
*/
protected function encodeComments(): string
{
return implode('', array_map(
fn(CommentExtension $commentExtension): string => $commentExtension->encode(),
$this->source->getComments()
));
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\GraphicControlExtension;
class GraphicControlExtensionEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(GraphicControlExtension $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
GraphicControlExtension::MARKER,
GraphicControlExtension::LABEL,
GraphicControlExtension::BLOCKSIZE,
$this->encodePackedField(),
$this->encodeDelay(),
$this->encodeTransparentColorIndex(),
GraphicControlExtension::TERMINATOR,
]);
}
/**
* Encode delay time
*/
protected function encodeDelay(): string
{
return pack('v*', $this->source->getDelay());
}
/**
* Encode transparent color index
*/
protected function encodeTransparentColorIndex(): string
{
return pack('C', $this->source->getTransparentColorIndex());
}
/**
* Encode packed field
*/
protected function encodePackedField(): string
{
return pack('C', bindec(implode('', [
str_pad('0', 3, '0', STR_PAD_LEFT),
str_pad(decbin($this->source->getDisposalMethod()->value), 3, '0', STR_PAD_LEFT),
(int) $this->source->getUserInput(),
(int) $this->source->getTransparentColorExistance(),
])));
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\Header;
class HeaderEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(Header $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return Header::SIGNATURE . $this->source->getVersion();
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Blocks\ImageData;
class ImageDataEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(ImageData $source)
{
$this->source = $source;
}
/**
* Encode current source
*
* @throws EncoderException
*/
public function encode(): string
{
if (!$this->source->hasBlocks()) {
throw new EncoderException("No data blocks in ImageData.");
}
return implode('', [
pack('C', $this->source->getLzwMinCodeSize()),
implode('', array_map(
fn(DataSubBlock $block): string => $block->encode(),
$this->source->getBlocks(),
)),
AbstractEntity::TERMINATOR,
]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\ImageDescriptor;
class ImageDescriptorEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(ImageDescriptor $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
ImageDescriptor::SEPARATOR,
$this->encodeLeft(),
$this->encodeTop(),
$this->encodeWidth(),
$this->encodeHeight(),
$this->encodePackedField(),
]);
}
/**
* Encode left value
*/
protected function encodeLeft(): string
{
return pack('v*', $this->source->getLeft());
}
/**
* Encode top value
*/
protected function encodeTop(): string
{
return pack('v*', $this->source->getTop());
}
/**
* Encode width value
*/
protected function encodeWidth(): string
{
return pack('v*', $this->source->getWidth());
}
/**
* Encode height value
*/
protected function encodeHeight(): string
{
return pack('v*', $this->source->getHeight());
}
/**
* Encode size of local color table
*/
protected function encodeLocalColorTableSize(): string
{
return str_pad(decbin($this->source->getLocalColorTableSize()), 3, '0', STR_PAD_LEFT);
}
/**
* Encode reserved field
*/
protected function encodeReservedField(): string
{
return str_pad('0', 2, '0', STR_PAD_LEFT);
}
/**
* Encode packed field
*/
protected function encodePackedField(): string
{
return pack('C', bindec(implode('', [
(int) $this->source->getLocalColorTableExistance(),
(int) $this->source->isInterlaced(),
(int) $this->source->getLocalColorTableSorted(),
$this->encodeReservedField(),
$this->encodeLocalColorTableSize(),
])));
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\LogicalScreenDescriptor;
class LogicalScreenDescriptorEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(LogicalScreenDescriptor $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
$this->encodeWidth(),
$this->encodeHeight(),
$this->encodePackedField(),
$this->encodeBackgroundColorIndex(),
$this->encodePixelAspectRatio(),
]);
}
/**
* Encode width of current instance
*/
protected function encodeWidth(): string
{
return pack('v*', $this->source->getWidth());
}
/**
* Encode height of current instance
*/
protected function encodeHeight(): string
{
return pack('v*', $this->source->getHeight());
}
/**
* Encode background color index of global color table
*/
protected function encodeBackgroundColorIndex(): string
{
return pack('C', $this->source->getBackgroundColorIndex());
}
/**
* Encode pixel aspect ratio
*/
protected function encodePixelAspectRatio(): string
{
return pack('C', $this->source->getPixelAspectRatio());
}
/**
* Return color resolution for encoding
*/
protected function encodeColorResolution(): string
{
return str_pad(decbin($this->source->getBitsPerPixel() - 1), 3, '0', STR_PAD_LEFT);
}
/**
* Encode size of global color table
*/
protected function encodeGlobalColorTableSize(): string
{
return str_pad(decbin($this->source->getGlobalColorTableSize()), 3, '0', STR_PAD_LEFT);
}
/**
* Encode packed field of current instance
*/
protected function encodePackedField(): string
{
return pack('C', bindec(implode('', [
(int) $this->source->getGlobalColorTableExistance(),
$this->encodeColorResolution(),
(int) $this->source->getGlobalColorTableSorted(),
$this->encodeGlobalColorTableSize(),
])));
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
class NetscapeApplicationExtensionEncoder extends ApplicationExtensionEncoder
{
/**
* Create new decoder instance
*/
public function __construct(NetscapeApplicationExtension $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return implode('', [
ApplicationExtension::MARKER,
ApplicationExtension::LABEL,
pack('C', $this->source->getBlockSize()),
$this->source->getApplication(),
implode('', array_map(fn(DataSubBlock $block): string => $block->encode(), $this->source->getBlocks())),
ApplicationExtension::TERMINATOR,
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\PlainTextExtension;
class PlainTextExtensionEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(PlainTextExtension $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
if (!$this->source->hasText()) {
return '';
}
return implode('', [
PlainTextExtension::MARKER,
PlainTextExtension::LABEL,
$this->encodeHead(),
$this->encodeTexts(),
PlainTextExtension::TERMINATOR,
]);
}
/**
* Encode head block
*/
protected function encodeHead(): string
{
return "\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
}
/**
* Encode text chunks
*/
protected function encodeTexts(): string
{
return implode('', array_map(
fn(string $text): string => pack('C', strlen($text)) . $text,
$this->source->getText(),
));
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\TableBasedImage;
class TableBasedImageEncoder extends AbstractEncoder
{
public function __construct(TableBasedImage $source)
{
$this->source = $source;
}
public function encode(): string
{
return implode('', [
$this->source->getImageDescriptor()->encode(),
$this->source->getColorTable() ? $this->source->getColorTable()->encode() : '',
$this->source->getImageData()->encode(),
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\Trailer;
class TrailerEncoder extends AbstractEncoder
{
/**
* Create new instance
*/
public function __construct(Trailer $source)
{
$this->source = $source;
}
/**
* Encode current source
*/
public function encode(): string
{
return Trailer::MARKER;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Exceptions;
class DecoderException extends RuntimeException
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Exceptions;
class EncoderException extends RuntimeException
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Exceptions;
class FormatException extends RuntimeException
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Exceptions;
class NotReadableException extends RuntimeException
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Exceptions;
class RuntimeException extends \RuntimeException
{
//
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use Intervention\Gif\Blocks\ColorTable;
use Intervention\Gif\Blocks\CommentExtension;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Blocks\Header;
use Intervention\Gif\Blocks\LogicalScreenDescriptor;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Blocks\Trailer;
class GifDataStream extends AbstractEntity
{
/**
* Create new instance
*
* @param array<FrameBlock> $frames
* @param array<CommentExtension> $comments
*/
public function __construct(
protected Header $header = new Header(),
protected LogicalScreenDescriptor $logicalScreenDescriptor = new LogicalScreenDescriptor(),
protected ?ColorTable $globalColorTable = null,
protected array $frames = [],
protected array $comments = []
) {
//
}
/**
* Get header
*/
public function getHeader(): Header
{
return $this->header;
}
/**
* Set header
*/
public function setHeader(Header $header): self
{
$this->header = $header;
return $this;
}
/**
* Get logical screen descriptor
*/
public function getLogicalScreenDescriptor(): LogicalScreenDescriptor
{
return $this->logicalScreenDescriptor;
}
/**
* Set logical screen descriptor
*/
public function setLogicalScreenDescriptor(LogicalScreenDescriptor $descriptor): self
{
$this->logicalScreenDescriptor = $descriptor;
return $this;
}
/**
* Return global color table if available else null
*/
public function getGlobalColorTable(): ?ColorTable
{
return $this->globalColorTable;
}
/**
* Set global color table
*/
public function setGlobalColorTable(ColorTable $table): self
{
$this->globalColorTable = $table;
$this->logicalScreenDescriptor->setGlobalColorTableExistance(true);
$this->logicalScreenDescriptor->setGlobalColorTableSize(
$table->getLogicalSize()
);
return $this;
}
/**
* Get main graphic control extension
*/
public function getMainApplicationExtension(): ?NetscapeApplicationExtension
{
foreach ($this->frames as $frame) {
if ($extension = $frame->getNetscapeExtension()) {
return $extension;
}
}
return null;
}
/**
* Get array of frames
*
* @return array<FrameBlock>
*/
public function getFrames(): array
{
return $this->frames;
}
/**
* Return array of "global" comments
*
* @return array<CommentExtension>
*/
public function getComments(): array
{
return $this->comments;
}
/**
* Return first frame
*/
public function getFirstFrame(): ?FrameBlock
{
if (!array_key_exists(0, $this->frames)) {
return null;
}
return $this->frames[0];
}
/**
* Add frame
*/
public function addFrame(FrameBlock $frame): self
{
$this->frames[] = $frame;
return $this;
}
/**
* Add comment extension
*/
public function addComment(CommentExtension $comment): self
{
$this->comments[] = $comment;
return $this;
}
/**
* Set the current data
*
* @param array<FrameBlock> $frames
*/
public function setFrames(array $frames): self
{
$this->frames = $frames;
return $this;
}
/**
* Get trailer
*/
public function getTrailer(): Trailer
{
return new Trailer();
}
/**
* Determine if gif is animated
*/
public function isAnimated(): bool
{
return count($this->getFrames()) > 1;
}
/**
* Determine if global color table is set
*/
public function hasGlobalColorTable(): bool
{
return !is_null($this->globalColorTable);
}
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use ArrayIterator;
use GdImage;
use Intervention\Gif\Exceptions\EncoderException;
use IteratorAggregate;
use Traversable;
/**
* @implements IteratorAggregate<GifDataStream>
*/
class Splitter implements IteratorAggregate
{
/**
* Single frames resolved to GifDataStream
*
* @var array<GifDataStream>
*/
protected array $frames = [];
/**
* Delays of each frame
*
* @var array<int>
*/
protected array $delays = [];
/**
* Create new instance
*/
public function __construct(protected GifDataStream $stream)
{
//
}
/**
* Static constructor method
*/
public static function create(GifDataStream $stream): self
{
return new self($stream);
}
/**
* Iterator
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->frames);
}
/**
* Get frames
*
* @return array<GifDataStream>
*/
public function getFrames(): array
{
return $this->frames;
}
/**
* Get delays
*
* @return array<int>
*/
public function getDelays(): array
{
return $this->delays;
}
/**
* Set stream of instance
*/
public function setStream(GifDataStream $stream): self
{
$this->stream = $stream;
return $this;
}
/**
* Split current stream into array of seperate streams for each frame
*/
public function split(): self
{
$this->frames = [];
foreach ($this->stream->getFrames() as $frame) {
// create separate stream for each frame
$gif = Builder::canvas(
$this->stream->getLogicalScreenDescriptor()->getWidth(),
$this->stream->getLogicalScreenDescriptor()->getHeight()
)->getGifDataStream();
// check if working stream has global color table
if ($this->stream->hasGlobalColorTable()) {
$gif->setGlobalColorTable($this->stream->getGlobalColorTable());
$gif->getLogicalScreenDescriptor()->setGlobalColorTableExistance(true);
$gif->getLogicalScreenDescriptor()->setGlobalColorTableSorted(
$this->stream->getLogicalScreenDescriptor()->getGlobalColorTableSorted()
);
$gif->getLogicalScreenDescriptor()->setGlobalColorTableSize(
$this->stream->getLogicalScreenDescriptor()->getGlobalColorTableSize()
);
$gif->getLogicalScreenDescriptor()->setBackgroundColorIndex(
$this->stream->getLogicalScreenDescriptor()->getBackgroundColorIndex()
);
$gif->getLogicalScreenDescriptor()->setPixelAspectRatio(
$this->stream->getLogicalScreenDescriptor()->getPixelAspectRatio()
);
$gif->getLogicalScreenDescriptor()->setBitsPerPixel(
$this->stream->getLogicalScreenDescriptor()->getBitsPerPixel()
);
}
// copy original frame
$gif->addFrame($frame);
$this->frames[] = $gif;
$this->delays[] = match (is_object($frame->getGraphicControlExtension())) {
true => $frame->getGraphicControlExtension()->getDelay(),
default => 0,
};
}
return $this;
}
/**
* Return array of GD library resources for each frame
*
* @throws EncoderException
* @return array<GdImage>
*/
public function toResources(): array
{
$resources = [];
foreach ($this->frames as $frame) {
$resource = imagecreatefromstring($frame->encode());
if ($resource === false) {
throw new EncoderException('Unable to extract animation frames.');
}
imagepalettetotruecolor($resource);
imagesavealpha($resource, true);
$resources[] = $resource;
}
return $resources;
}
/**
* Return array of coalesced GD library resources for each frame
*
* @throws EncoderException
* @return array<GdImage>
*/
public function coalesceToResources(): array
{
$resources = $this->toResources();
// static gif files don't need to be coalesced
if (count($resources) === 1) {
return $resources;
}
$width = imagesx($resources[0]);
$height = imagesy($resources[0]);
$transparent = imagecolortransparent($resources[0]);
foreach ($resources as $key => $resource) {
// get meta data
$gif = $this->frames[$key];
$descriptor = $gif->getFirstFrame()->getImageDescriptor();
$offset_x = $descriptor->getLeft();
$offset_y = $descriptor->getTop();
$w = $descriptor->getWidth();
$h = $descriptor->getHeight();
if (in_array($this->getDisposalMethod($gif), [DisposalMethod::NONE, DisposalMethod::PREVIOUS])) {
if ($key >= 1) {
// create normalized gd image
$canvas = imagecreatetruecolor($width, $height);
if (imagecolortransparent($resource) != -1) {
$transparent = imagecolortransparent($resource);
} else {
$transparent = imagecolorallocatealpha($resource, 255, 0, 255, 127);
}
if (!is_int($transparent)) {
throw new EncoderException('Animation frames cannot be converted into resources.');
}
// fill with transparent
imagefill($canvas, 0, 0, $transparent);
imagecolortransparent($canvas, $transparent);
imagealphablending($canvas, true);
// insert last as base
imagecopy(
$canvas,
$resources[$key - 1],
0,
0,
0,
0,
$width,
$height
);
// insert resource
imagecopy(
$canvas,
$resource,
$offset_x,
$offset_y,
0,
0,
$w,
$h
);
} else {
imagealphablending($resource, true);
$canvas = $resource;
}
} else {
// create normalized gd image
$canvas = imagecreatetruecolor($width, $height);
if (imagecolortransparent($resource) != -1) {
$transparent = imagecolortransparent($resource);
} else {
$transparent = imagecolorallocatealpha($resource, 255, 0, 255, 127);
}
if (!is_int($transparent)) {
throw new EncoderException('Animation frames cannot be converted into resources.');
}
// fill with transparent
imagefill($canvas, 0, 0, $transparent);
imagecolortransparent($canvas, $transparent);
imagealphablending($canvas, true);
// insert frame resource
imagecopy(
$canvas,
$resource,
$offset_x,
$offset_y,
0,
0,
$w,
$h
);
}
$resources[$key] = $canvas;
}
return $resources;
}
/**
* Find and return disposal method of given gif data stream
*/
private function getDisposalMethod(GifDataStream $gif): DisposalMethod
{
return $gif->getFirstFrame()->getGraphicControlExtension()->getDisposalMethod();
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Traits;
use Intervention\Gif\Decoders\AbstractDecoder;
use Intervention\Gif\Exceptions\DecoderException;
trait CanDecode
{
/**
* Decode current instance
*
* @throws DecoderException
*/
public static function decode(mixed $source, ?int $length = null): mixed
{
return self::getDecoder($source, $length)->decode();
}
/**
* Get decoder for current instance
*
* @throws DecoderException
*/
protected static function getDecoder(mixed $source, ?int $length = null): AbstractDecoder
{
$classname = sprintf('Intervention\Gif\Decoders\%sDecoder', self::getShortClassname());
if (!class_exists($classname)) {
throw new DecoderException("Decoder for '" . static::class . "' not found.");
}
$decoder = new $classname($source, $length);
if (!($decoder instanceof AbstractDecoder)) {
throw new DecoderException("Decoder for '" . static::class . "' not found.");
}
return $decoder;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Traits;
use Intervention\Gif\Encoders\AbstractEncoder;
use Intervention\Gif\Exceptions\EncoderException;
trait CanEncode
{
/**
* Encode current entity
*
* @throws EncoderException
*/
public function encode(): string
{
return $this->getEncoder()->encode();
}
/**
* Get encoder object for current entity
*
* @throws EncoderException
*/
protected function getEncoder(): AbstractEncoder
{
$classname = sprintf('Intervention\Gif\Encoders\%sEncoder', $this->getShortClassname());
if (!class_exists($classname)) {
throw new EncoderException("Encoder for '" . $this::class . "' not found.");
}
$encoder = new $classname($this);
if (!($encoder instanceof AbstractEncoder)) {
throw new EncoderException("Encoder for '" . $this::class . "' not found.");
}
return $encoder;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Traits;
use Intervention\Gif\Exceptions\RuntimeException;
trait CanHandleFiles
{
/**
* Determines if input is file path
*/
private static function isFilePath(mixed $input): bool
{
return is_string($input) && !self::hasNullBytes($input) && @is_file($input);
}
/**
* Determine if given string contains null bytes
*/
private static function hasNullBytes(string $string): bool
{
return str_contains($string, chr(0));
}
/**
* Create file pointer from given gif image data
*
* @throws RuntimeException
*/
private static function getHandleFromData(string $data): mixed
{
$handle = fopen('php://temp', 'r+');
if ($handle === false) {
throw new RuntimeException('Unable to create tempory file handle.');
}
fwrite($handle, $data);
rewind($handle);
return $handle;
}
/**
* Create file pounter from given file path
*/
private static function getHandleFromFilePath(string $path): mixed
{
return fopen($path, 'rb');
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-present Oliver Vogel
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,51 @@
{
"name": "intervention/image",
"description": "PHP Image Processing",
"homepage": "https://image.intervention.io",
"keywords": [
"image",
"gd",
"imagick",
"watermark",
"thumbnail",
"resize"
],
"license": "MIT",
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io"
}
],
"require": {
"php": "^8.1",
"ext-mbstring": "*",
"intervention/gif": "^4.2"
},
"require-dev": {
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"squizlabs/php_codesniffer": "^3.8",
"slevomat/coding-standard": "~8.0"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Intervention\\Image\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,91 @@
# Intervention Image
## PHP Image Processing
[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image)
[![Build Status](https://github.com/Intervention/image/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image/actions)
[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats)
[![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp)
Intervention Image is a **PHP image processing library** that provides a simple
and expressive way to create, edit, and compose images. It comes with a universal
interface for the two most popular PHP image manipulation extensions. You can
choose between the GD library or Imagick as the base layer for all operations.
- Simple interface for common image editing tasks
- Interchangeable driver architecture
- Support for animated images
- Framework-agnostic
- PSR-12 compliant
## Installation
You can easily install this library using [Composer](https://getcomposer.org).
Simply request the package with the following command:
```bash
composer require intervention/image
```
## Getting Started
Learn the [basics](https://image.intervention.io/v3/basics/instantiation/) on
how to use Intervention Image and more with the [official
documentation](https://image.intervention.io/v3/).
## Code Examples
```php
use Intervention\Image\ImageManager;
// create image manager with desired driver
$manager = new ImageManager(
new Intervention\Image\Drivers\Gd\Driver()
);
// open an image file
$image = $manager->read('images/example.gif');
// resize image instance
$image->resize(height: 300);
// insert a watermark
$image->place('images/watermark.png');
// encode edited image
$encoded = $image->toJpg();
// save encoded image
$encoded->save('images/example.jpg');
```
## Requirements
Before you begin with the installation make sure that your server environment
supports the following requirements.
- PHP >= 8.1
- Mbstring PHP Extension
- Image Processing PHP Extension
## Supported Image Libraries
Depending on your environment Intervention Image lets you choose between
different image processing extensions.
- GD Library
- Imagick PHP extension
- [libvips](https://github.com/Intervention/image-driver-vips)
## Security
If you discover any security related issues, please email oliver@intervention.io directly.
## Authors
This library is developed and maintained by [Oliver Vogel](https://intervention.io)
Thanks to the community of [contributors](https://github.com/Intervention/image/graphs/contributors) who have helped to improve this project.
## License
Intervention Image is licensed under the [MIT License](LICENSE).

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ColorspaceAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class HeightAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class PixelColorAnalyzer extends SpecializableAnalyzer
{
public function __construct(
public int $x,
public int $y,
public int $frame_key = 0
) {
//
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class PixelColorsAnalyzer extends SpecializableAnalyzer
{
public function __construct(
public int $x,
public int $y
) {
//
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ProfileAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class ResolutionAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Analyzers;
use Intervention\Image\Drivers\SpecializableAnalyzer;
class WidthAnalyzer extends SpecializableAnalyzer
{
//
}

View File

@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Intervention\Image\Interfaces\CollectionInterface;
use ArrayIterator;
use Countable;
use Traversable;
use IteratorAggregate;
/**
* @implements IteratorAggregate<int|string, mixed>
*/
class Collection implements CollectionInterface, IteratorAggregate, Countable
{
/**
* Create new collection object
*
* @param array<int|string, mixed> $items
* @return void
*/
public function __construct(protected array $items = [])
{
//
}
/**
* Static constructor
*
* @param array<int|string, mixed> $items
* @return self<int|string, mixed>
*/
public static function create(array $items = []): self
{
return new self($items);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::has()
*/
public function has(int|string $key): bool
{
return array_key_exists($key, $this->items);
}
/**
* Returns Iterator
*
* @return Traversable<int|string, mixed>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::toArray()
*/
public function toArray(): array
{
return $this->items;
}
/**
* Count items in collection
*/
public function count(): int
{
return count($this->items);
}
/**
* Append new item to collection
*
* @return CollectionInterface<int|string, mixed>
*/
public function push(mixed $item): CollectionInterface
{
$this->items[] = $item;
return $this;
}
/**
* Return first item in collection
*/
public function first(): mixed
{
if ($item = reset($this->items)) {
return $item;
}
return null;
}
/**
* Returns last item in collection
*/
public function last(): mixed
{
if ($item = end($this->items)) {
return $item;
}
return null;
}
/**
* Return item at given position starting at 0
*/
public function getAtPosition(int $key = 0, mixed $default = null): mixed
{
if ($this->count() == 0) {
return $default;
}
$positions = array_values($this->items);
if (!array_key_exists($key, $positions)) {
return $default;
}
return $positions[$key];
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::get()
*/
public function get(int|string $query, mixed $default = null): mixed
{
if ($this->count() == 0) {
return $default;
}
if (is_int($query) && array_key_exists($query, $this->items)) {
return $this->items[$query];
}
if (is_string($query) && !str_contains($query, '.')) {
return array_key_exists($query, $this->items) ? $this->items[$query] : $default;
}
$query = explode('.', (string) $query);
$result = $default;
$items = $this->items;
foreach ($query as $key) {
if (!is_array($items) || !array_key_exists($key, $items)) {
$result = $default;
break;
}
$result = $items[$key];
$items = $result;
}
return $result;
}
/**
* Map each item of collection by given callback
*/
public function map(callable $callback): self
{
return new self(
array_map(
fn(mixed $item) => $callback($item),
$this->items,
)
);
}
/**
* Run callback on each item of the collection an remove it if it does not return true
*/
public function filter(callable $callback): self
{
return new self(
array_filter(
$this->items,
fn(mixed $item) => $callback($item),
)
);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::empty()
*/
public function empty(): CollectionInterface
{
$this->items = [];
return $this;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::slice()
*/
public function slice(int $offset, ?int $length = null): CollectionInterface
{
$this->items = array_slice($this->items, $offset, $length);
return $this;
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use ReflectionClass;
use Stringable;
abstract class AbstractColor implements ColorInterface, Stringable
{
/**
* Color channels
*
* @var array<ColorChannelInterface>
*/
protected array $channels;
/**
* {@inheritdoc}
*
* @see ColorInterface::channels()
*/
public function channels(): array
{
return $this->channels;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::channel()
*/
public function channel(string $classname): ColorChannelInterface
{
$channels = array_filter(
$this->channels(),
fn(ColorChannelInterface $channel): bool => $channel::class === $classname,
);
if (count($channels) == 0) {
throw new ColorException('Color channel ' . $classname . ' could not be found.');
}
return reset($channels);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::normalize()
*/
public function normalize(): array
{
return array_map(
fn(ColorChannelInterface $channel): float => $channel->normalize(),
$this->channels(),
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toArray()
*/
public function toArray(): array
{
return array_map(
fn(ColorChannelInterface $channel): int => $channel->value(),
$this->channels()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::convertTo()
*/
public function convertTo(string|ColorspaceInterface $colorspace): ColorInterface
{
$colorspace = match (true) {
is_object($colorspace) => $colorspace,
default => new $colorspace(),
};
return $colorspace->importColor($this);
}
/**
* Show debug info for the current color
*
* @return array<string, int>
*/
public function __debugInfo(): array
{
return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) {
$key = strtolower((new ReflectionClass($item))->getShortName());
$result[$key] = $item->value();
return $result;
}, []);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Stringable;
abstract class AbstractColorChannel implements ColorChannelInterface, Stringable
{
protected int $value;
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::__construct()
*/
public function __construct(?int $value = null, ?float $normalized = null)
{
$this->value = $this->validate(
match (true) {
is_null($value) && is_numeric($normalized) => intval(round($normalized * $this->max())),
is_numeric($value) && is_null($normalized) => $value,
default => throw new ColorException('Color channels must either have a value or a normalized value')
}
);
}
/**
* Alias of value()
*/
public function toInt(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::normalize()
*/
public function normalize(int $precision = 32): float
{
return round(($this->value() - $this->min()) / ($this->max() - $this->min()), $precision);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::validate()
*/
public function validate(mixed $value): mixed
{
if ($value < $this->min() || $value > $this->max()) {
throw new ColorException('Color channel value must be in range ' . $this->min() . ' to ' . $this->max());
}
return $value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::toString()
*/
public function toString(): string
{
return (string) $this->value();
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Cyan extends AbstractColorChannel
{
public function min(): int
{
return 0;
}
public function max(): int
{
return 100;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Key extends Cyan
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Magenta extends Cyan
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
class Yellow extends Cyan
{
//
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Cmyk\Channels\Cyan;
use Intervention\Image\Colors\Cmyk\Channels\Magenta;
use Intervention\Image\Colors\Cmyk\Channels\Yellow;
use Intervention\Image\Colors\Cmyk\Channels\Key;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new instance
*
* @return void
*/
public function __construct(int $c, int $m, int $y, int $k)
{
/** @throws void */
$this->channels = [
new Cyan($c),
new Magenta($m),
new Yellow($y),
new Key($k),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toHex()
*/
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* Return the CMYK cyan channel
*/
public function cyan(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Cyan::class);
}
/**
* Return the CMYK magenta channel
*/
public function magenta(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Magenta::class);
}
/**
* Return the CMYK yellow channel
*/
public function yellow(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Yellow::class);
}
/**
* Return the CMYK key channel
*/
public function key(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Key::class);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'cmyk(%d%%, %d%%, %d%%, %d%%)',
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
$this->key()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return 0 === array_sum([
$this->cyan()->value(),
$this->magenta()->value(),
$this->yellow()->value(),
]);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Cyan::class,
Channels\Magenta::class,
Channels\Yellow::class,
Channels\Key::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::createColor()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized,
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
HslColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): CmykColor
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
$c = (255 - $color->red()->value()) / 255.0 * 100;
$m = (255 - $color->green()->value()) / 255.0 * 100;
$y = (255 - $color->blue()->value()) / 255.0 * 100;
$k = intval(round(min([$c, $m, $y])));
$c = intval(round($c - $k));
$m = intval(round($m - $k));
$y = intval(round($y - $k));
return new CmykColor($c, $m, $y, $k);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Decoders;
use Intervention\Image\Colors\Cmyk\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode CMYK color strings
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^cmyk\((?P<c>[0-9\.]+%?), ?(?P<m>[0-9\.]+%?), ?(?P<y>[0-9\.]+%?), ?(?P<k>[0-9\.]+%?)\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function (string $value): int {
return intval(round(floatval(trim(str_replace('%', '', $value)))));
}, [$matches['c'], $matches['m'], $matches['y'], $matches['k']]);
return new Color(...$values);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Hue extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 360;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Luminance extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Saturation extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Hsl\Channels\Hue;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object
*
* @return void
*/
public function __construct(int $h, int $s, int $l)
{
/** @throws void */
$this->channels = [
new Hue($h),
new Saturation($s),
new Luminance($l),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* Return the Hue channel
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Luminance channel
*/
public function luminance(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Luminance::class);
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'hsl(%d, %d%%, %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->luminance()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return $this->saturation()->value() == 0;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Luminance::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HsvColor::class => $this->importHsvColor($color),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of rgb channels
$values = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalize(),
$color->channels(),
);
// take only RGB
$values = array_slice($values, 0, 3);
// calculate Luminance
$min = min(...$values);
$max = max(...$values);
$luminance = ($max + $min) / 2;
$delta = $max - $min;
// calculate saturation
$saturation = match (true) {
$delta == 0 => 0,
default => $delta / (1 - abs(2 * $luminance - 1)),
};
// calculate hue
[$r, $g, $b] = $values;
$hue = match (true) {
($delta == 0) => 0,
($max == $r) => 60 * fmod((($g - $b) / $delta), 6),
($max == $g) => 60 * ((($b - $r) / $delta) + 2),
($max == $b) => 60 * ((($r - $g) / $delta) + 4),
default => 0,
};
$hue = ($hue + 360) % 360; // normalize hue
return new Color(
intval(round($hue)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
);
}
/**
* @throws ColorException
*/
protected function importHsvColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HsvColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of hsv channels
[$h, $s, $v] = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalize(),
$color->channels(),
);
// calculate Luminance
$luminance = (2 - $s) * $v / 2;
// calculate Saturation
$saturation = match (true) {
$luminance == 0 => $s,
$luminance == 1 => 0,
$luminance < .5 => $s * $v / ($luminance * 2),
default => $s * $v / (2 - $luminance * 2),
};
return new Color(
intval(round($h * 360)),
intval(round($saturation * 100)),
intval(round($luminance * 100)),
);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsl\Decoders;
use Intervention\Image\Colors\Hsl\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode hsl color strings
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^hsl\((?P<h>[0-9\.]+), ?(?P<s>[0-9\.]+%?), ?(?P<l>[0-9\.]+%?)\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function (string $value): int {
return match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
};
}, [$matches['h'], $matches['s'], $matches['l']]);
return new Color(...$values);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Hue extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 360;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Saturation extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Channels;
use Intervention\Image\Colors\AbstractColorChannel;
class Value extends AbstractColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public function min(): int
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public function max(): int
{
return 100;
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\AbstractColor;
use Intervention\Image\Colors\Hsv\Channels\Hue;
use Intervention\Image\Colors\Hsv\Channels\Saturation;
use Intervention\Image\Colors\Hsv\Channels\Value;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\InputHandler;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Color extends AbstractColor
{
/**
* Create new color object
*
* @return void
*/
public function __construct(int $h, int $s, int $v)
{
/** @throws void */
$this->channels = [
new Hue($h),
new Saturation($s),
new Value($v),
];
}
/**
* {@inheritdoc}
*
* @see ColorInterface::colorspace()
*/
public function colorspace(): ColorspaceInterface
{
return new Colorspace();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::create()
*/
public static function create(mixed $input): ColorInterface
{
return InputHandler::withDecoders([
Decoders\StringColorDecoder::class,
])->handle($input);
}
/**
* Return the Hue channel
*/
public function hue(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Hue::class);
}
/**
* Return the Saturation channel
*/
public function saturation(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Saturation::class);
}
/**
* Return the Value channel
*/
public function value(): ColorChannelInterface
{
/** @throws void */
return $this->channel(Value::class);
}
public function toHex(string $prefix = ''): string
{
return $this->convertTo(RgbColorspace::class)->toHex($prefix);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toString()
*/
public function toString(): string
{
return sprintf(
'hsv(%d, %d%%, %d%%)',
$this->hue()->value(),
$this->saturation()->value(),
$this->value()->value()
);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isGreyscale()
*/
public function isGreyscale(): bool
{
return $this->saturation()->value() == 0;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return false;
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
class Colorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace
*
* @var array<string>
*/
public static array $channels = [
Channels\Hue::class,
Channels\Saturation::class,
Channels\Value::class
];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::colorFromNormalized()
*/
public function colorFromNormalized(array $normalized): ColorInterface
{
return new Color(...array_map(
fn(string $classname, float $value_normalized) => (new $classname(normalized: $value_normalized))->value(),
self::$channels,
$normalized
));
}
/**
* @throws ColorException
*/
public function importColor(ColorInterface $color): ColorInterface
{
return match ($color::class) {
CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)),
RgbColor::class => $this->importRgbColor($color),
HslColor::class => $this->importHslColor($color),
default => $color,
};
}
/**
* @throws ColorException
*/
protected function importRgbColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof RgbColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of rgb channels
$values = array_map(fn(ColorChannelInterface $channel): float => $channel->normalize(), $color->channels());
// take only RGB
$values = array_slice($values, 0, 3);
// calculate chroma
$min = min(...$values);
$max = max(...$values);
$chroma = $max - $min;
// calculate value
$v = 100 * $max;
if ($chroma == 0) {
// greyscale color
return new Color(0, 0, intval(round($v)));
}
// calculate saturation
$s = 100 * ($chroma / $max);
// calculate hue
[$r, $g, $b] = $values;
$h = match (true) {
($r == $min) => 3 - (($g - $b) / $chroma),
($b == $min) => 1 - (($r - $g) / $chroma),
default => 5 - (($b - $r) / $chroma),
} * 60;
return new Color(
intval(round($h)),
intval(round($s)),
intval(round($v))
);
}
/**
* @throws ColorException
*/
protected function importHslColor(ColorInterface $color): ColorInterface
{
if (!($color instanceof HslColor)) {
throw new ColorException('Unabled to import color of type ' . $color::class . '.');
}
// normalized values of hsl channels
[$h, $s, $l] = array_map(
fn(ColorChannelInterface $channel): float => $channel->normalize(),
$color->channels()
);
$v = $l + $s * min($l, 1 - $l);
$s = ($v == 0) ? 0 : 2 * (1 - $l / $v);
return $this->colorFromNormalized([$h, $s, $v]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Hsv\Decoders;
use Intervention\Image\Colors\Hsv\Color;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class StringColorDecoder extends AbstractDecoder implements DecoderInterface
{
/**
* Decode hsv/hsb color strings
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
$pattern = '/^hs(v|b)\((?P<h>[0-9\.]+), ?(?P<s>[0-9\.]+%?), ?(?P<v>[0-9\.]+%?)\)$/i';
if (preg_match($pattern, $input, $matches) != 1) {
throw new DecoderException('Unable to decode input');
}
$values = array_map(function (string $value): int {
return match (strpos($value, '%')) {
false => intval(trim($value)),
default => intval(trim(str_replace('%', '', $value))),
};
}, [$matches['h'], $matches['s'], $matches['v']]);
return new Color(...$values);
}
}

Some files were not shown because too many files have changed in this diff Show More