refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

129
vendor/intervention/gif/AGENTS.md vendored Normal file
View File

@@ -0,0 +1,129 @@
# Intervention Gif Agent Guide
This document provides a guide for software engineering agents working on the Intervention Gif codebase.
## 1. Project Overview
**intervention/gif** -- Native PHP GIF encoder/decoder library. Parses and generates
GIF binary data. Part of the [Intervention Image](https://github.com/Intervention/image) ecosystem.
- **Language:** PHP 8.3+ | **Namespace:** `Intervention\Gif`
### 1.1 Architecture
The codebase mirrors the GIF89a binary specification. Each GIF format block has three
corresponding classes:
- `src/Blocks/<Name>.php` -- Data model (value object)
- `src/Decoders/<Name>Decoder.php` -- Binary stream reader
- `src/Encoders/<Name>Encoder.php` -- Binary stream writer
Encoder/decoder resolution is convention-based via `CanDecode`/`CanEncode` traits.
All block models extend `AbstractEntity` (which implements `Stringable`).
## 2. Development Environment
The project uses Composer to manage dependencies. These dependencies are installed automatically when using the Docker test runners.
## 3. Build, Lint, and Test Commands
The following commands are used to ensure code quality and correctness.
### 3.1. Testing (PHPUnit)
The project uses PHPUnit for unit and feature testing.
- **Run all tests:**
```bash
docker compose run --rm tests
```
- **Run a single test file:**
To run a specific test file, provide the path to the file.
```bash
docker compose run --rm tests tests/Unit/BuilderTest.php
```
- **Run a single test method:**
Use the `--filter` option to run a specific test method by its name.
```bash
docker compose run --rm tests tests/Unit/BuilderTest.php --filter testMethodName
```
- **Check test coverage:**
```bash
docker compose run --rm coverage
```
### 3.2. Static Analysis (PHPStan)
PHPStan is used for static analysis to find potential bugs.
- **Run static analysis:**
```bash
docker compose run --rm analysis
```
### 3.3. Coding Standards (PHP CodeSniffer)
The project adheres to the PSR-12 coding standard with additional rules. PHP CodeSniffer is used to enforce these standards.
- **Check for coding standard violations:**
```bash
docker compose run --rm standards
```
## 4. Code Style and Conventions
Consistency is key. Adhere to the following guidelines when writing code.
### 4.1. Formatting
- **PSR-12:** The primary coding standard is PSR-12.
- **Indentation:** Use 4 spaces for indentation, not tabs.
- **Line Endings:** Use Unix-style line endings (LF).
- **Strict Types:** All PHP files must start with `declare(strict_types=1);`.
- **Class Structure:** Follow the ordering defined in `phpcs.xml`:
1. `uses`
2. `enum cases`
3. `constants`
4. `static properties`
5. `properties`
6. `constructor`
7. `static constructors`
8. `methods`
9. `magic methods`
### 4.2. Naming Conventions
- **Classes:** `PascalCase`.
- **Methods:** `camelCase`.
- **Variables:** `camelCase`.
- **Constants:** `UPPER_CASE` with underscore separators.
- **File Names:** File names must match the class name they contain (e.g., `MyClass.php` for `class MyClass`).
### 4.3. Imports
- **One class per `use` statement:** Do not group multiple classes in a single `use` statement.
- **No leading backslash:** `use` statements must not start with a backslash.
- **Order:** `use` statements should be ordered alphabetically. Unused imports must be removed.
### 4.4. Types and Type Hinting
- **Strict Typing:** All code should be strictly typed.
- **Parameter Types:** All method parameters must have a type hint.
- **Return Types:** All methods must have a return type hint.
- **Property Types:** All class properties must have a type hint.
- **Nullable Types:** Use nullable types (`?TypeName`) when a `null` value is explicitly allowed.
### 4.5. Error Handling
- Exceptions should be used for error handling.
- When catching exceptions, be as specific as possible. Avoid catching generic `\Exception` or `\Throwable`.
- Exception messages should be clear and descriptive.
### 4.6. PHPDoc (DocBlocks)
- PHPDoc blocks are required for all classes, properties, and methods.
- Follow the annotation order defined in `phpcs.xml`.
- Use DocBlocks to provide context and explain complex logic. Do not restate the obvious from the code signature.

21
vendor/intervention/gif/LICENSE vendored Normal 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.

100
vendor/intervention/gif/README.md vendored Normal file
View File

@@ -0,0 +1,100 @@
# 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.3
## Development & Testing
With this package comes a Docker docker test runners. 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).

44
vendor/intervention/gif/composer.json vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "intervention/gif",
"description": "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.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0",
"phpstan/phpstan": "^2.1",
"squizlabs/php_codesniffer": "^4",
"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
}
}
}

6
vendor/intervention/gif/entrypoint.sh vendored Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
composer install --quiet
exec "$@"

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
backupGlobals="false"
backupStaticProperties="false"
beStrictAboutTestsThatDoNotTestAnything="false"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
colors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnPhpunitDeprecations="true"
displayDetailsOnTestsThatTriggerWarnings="true"
processIsolation="false"
stopOnFailure="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,42 @@
<?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 ReflectionException;
use Stringable;
abstract class AbstractEntity implements Stringable
{
use CanEncode;
use CanDecode;
public const TERMINATOR = "\x00";
/**
* Get short classname of current instance.
*/
public static function shortClassname(): ?string
{
try {
return (new ReflectionClass(static::class))->getShortName();
} catch (ReflectionException) {
return null;
}
}
/**
* 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\StateException;
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 blockSize(): int
{
return strlen($this->application);
}
/**
* Set application name.
*/
public function setApplication(string $value): self
{
$this->application = $value;
return $this;
}
/**
* Get application name.
*/
public function application(): 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 blocks(): array
{
return $this->blocks;
}
/**
* Get first block of ApplicationExtension.
*
* @throws StateException
*/
public function firstBlock(): DataSubBlock
{
if (!array_key_exists(0, $this->blocks)) {
throw new StateException('Failed to retrieve data sub block');
}
return $this->blocks[0];
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class Color extends AbstractEntity
{
/**
* Create new instance.
*
* @throws InvalidArgumentException
*/
public function __construct(
protected int $r = 0,
protected int $g = 0,
protected int $b = 0
) {
if ($r < 0 || $r > 255) {
throw new InvalidArgumentException('Color channel red must be in range 0 to 255');
}
if ($g < 0 || $g > 255) {
throw new InvalidArgumentException('Color channel green must be in range 0 to 255');
}
if ($b < 0 || $b > 255) {
throw new InvalidArgumentException('Color channel blue must be in range 0 to 255');
}
}
/**
* Get red value.
*/
public function red(): int
{
return $this->r;
}
/**
* Set red value.
*/
public function setRed(int $value): self
{
$this->r = $value;
return $this;
}
/**
* Get green value.
*/
public function green(): int
{
return $this->g;
}
/**
* Set green value.
*/
public function setGreen(int $value): self
{
$this->g = $value;
return $this;
}
/**
* Get blue value.
*/
public function blue(): 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 hash(): string
{
return md5(strval($this->r . $this->g . $this->b));
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class ColorTable extends AbstractEntity
{
/**
* Create new instance.
*
* @param array<Color> $colors
*/
public function __construct(protected array $colors = [])
{
//
}
/**
* Return array of current colors.
*
* @return array<Color>
*/
public function colors(): array
{
return array_values($this->colors);
}
/**
* Add color to table.
*
* @throws InvalidArgumentException
*/
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 logicalSize(): 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 byteSize(): int
{
if (!$this->hasColors()) {
return 0;
}
return 3 * pow(2, $this->logicalSize() + 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 comments(): array
{
return $this->comments;
}
/**
* Get one comment by key.
*/
public function comment(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\InvalidArgumentException;
class DataSubBlock extends AbstractEntity
{
/**
* Create new instance.
*
* @throws InvalidArgumentException
*/
public function __construct(protected string $value)
{
if ($this->size() > 255) {
throw new InvalidArgumentException(
'Data Sub-Block can not have a block size larger than 255 bytes'
);
}
}
/**
* Return size of current block.
*/
public function size(): int
{
return strlen($this->value);
}
/**
* Return block value.
*/
public function value(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,254 @@
<?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()
) {
//
}
/**
* Add entity to block.
*/
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 applicationExtensions(): array
{
return $this->applicationExtensions;
}
/**
* Return comment extensions of current frame block.
*
* @return array<CommentExtension>
*/
public function commentExtensions(): 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 graphicControlExtension(): ?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 imageDescriptor(): 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 colorTable(): ?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 imageData(): 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 plainTextExtension(): ?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 netscapeExtension(): ?NetscapeApplicationExtension
{
$extensions = array_filter(
$this->applicationExtensions,
fn(ApplicationExtension $extension): bool => $extension instanceof NetscapeApplicationExtension,
);
return count($extensions) > 0 ? reset($extensions) : null;
}
/**
* Set the table based image of the current frame block.
*/
public function setTableBasedImage(TableBasedImage $tableBasedImage): self
{
$this->setImageDescriptor($tableBasedImage->imageDescriptor());
$colorTable = $tableBasedImage->colorTable();
if ($colorTable !== null) {
$this->setColorTable($colorTable);
}
$this->setImageData($tableBasedImage->imageData());
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 delay(): int
{
return $this->delay;
}
/**
* Set disposal method.
*/
public function setDisposalMethod(DisposalMethod $method): self
{
$this->disposalMethod = $method;
return $this;
}
/**
* Get disposal method.
*/
public function disposalMethod(): DisposalMethod
{
return $this->disposalMethod;
}
/**
* Get transparent color index.
*/
public function transparentColorIndex(): 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 transparentColorExistance(): 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 userInput(): 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 version(): 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 $lzwMinCodeSize;
/**
* Sub blocks.
*
* @var array<DataSubBlock>
*/
protected array $blocks = [];
/**
* Get LZW min. code size.
*/
public function lzwMinCodeSize(): int
{
return $this->lzwMinCodeSize;
}
/**
* Set lzw min. code size.
*/
public function setLzwMinCodeSize(int $size): self
{
$this->lzwMinCodeSize = $size;
return $this;
}
/**
* Get current data sub blocks.
*
* @return array<DataSubBlock>
*/
public function blocks(): 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,205 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\InvalidArgumentException;
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 width(): int
{
return $this->width;
}
/**
* Get current width.
*/
public function height(): int
{
return $this->height;
}
/**
* Get current Top.
*/
public function top(): int
{
return $this->top;
}
/**
* Get current Left.
*/
public function left(): int
{
return $this->left;
}
/**
* Set size of current instance.
*
* @throws InvalidArgumentException
*/
public function setSize(int $width, int $height): self
{
if ($width <= 0) {
throw new InvalidArgumentException('Width in ' . $this::class . ' must be larger than 0');
}
if ($height <= 0) {
throw new InvalidArgumentException('Height in ' . $this::class . ' must be larger than 0');
}
$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 localColorTableExistance(): bool
{
return $this->localColorTableExistance;
}
/**
* Alias for localColorTableExistance.
*/
public function hasLocalColorTable(): bool
{
return $this->localColorTableExistance();
}
/**
* 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 localColorTableSorted(): 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 localColorTableSize(): int
{
return $this->localColorTableSize;
}
/**
* Get byte size of global color table.
*/
public function localColorTableByteSize(): int
{
return 3 * pow(2, $this->localColorTableSize() + 1);
}
/**
* Set size of local color table.
*/
public function setLocalColorTableSize(int $size): self
{
$this->localColorTableSize = $size;
return $this;
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\InvalidArgumentException;
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.
*
* @throws InvalidArgumentException
*/
public function setSize(int $width, int $height): self
{
if ($width <= 0) {
throw new InvalidArgumentException('Width in ' . $this::class . ' must be larger than 0');
}
if ($height <= 0) {
throw new InvalidArgumentException('Height in ' . $this::class . ' must be larger than 0');
}
$this->width = $width;
$this->height = $height;
return $this;
}
/**
* Get width of current instance.
*/
public function width(): int
{
return $this->width;
}
/**
* Get height of current instance.
*/
public function height(): int
{
return $this->height;
}
/**
* Determine if global color table is present.
*/
public function globalColorTableExistance(): bool
{
return $this->globalColorTableExistance;
}
/**
* Alias of globalColorTableExistance.
*/
public function hasGlobalColorTable(): bool
{
return $this->globalColorTableExistance();
}
/**
* 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 globalColorTableSorted(): 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 globalColorTableSize(): int
{
return $this->globalColorTableSize;
}
/**
* Get byte size of global color table.
*/
public function globalColorTableByteSize(): int
{
return 3 * pow(2, $this->globalColorTableSize() + 1);
}
/**
* Set size of global color table.
*/
public function setGlobalColorTableSize(int $size): self
{
$this->globalColorTableSize = $size;
return $this;
}
/**
* Get background color index.
*/
public function backgroundColorIndex(): 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 pixelAspectRatio(): int
{
return $this->pixelAspectRatio;
}
/**
* Set pixel aspect ratio.
*/
public function setPixelAspectRatio(int $ratio): self
{
$this->pixelAspectRatio = $ratio;
return $this;
}
/**
* Get color resolution.
*/
public function bitsPerPixel(): int
{
return $this->bitsPerPixel;
}
/**
* Set color resolution.
*/
public function setBitsPerPixel(int $value): self
{
$this->bitsPerPixel = $value;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Blocks;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
use Intervention\Gif\Exceptions\StateException;
class NetscapeApplicationExtension extends ApplicationExtension
{
public const IDENTIFIER = "NETSCAPE";
public const AUTH_CODE = "2.0";
public const SUB_BLOCK_PREFIX = "\x01";
/**
* Create new instance.
*/
public function __construct()
{
try {
$this->setApplication(self::IDENTIFIER . self::AUTH_CODE);
$this->setBlocks([new DataSubBlock(self::SUB_BLOCK_PREFIX . "\x00\x00")]);
} catch (InvalidArgumentException) {
// ignore exception because of hard coded input
}
}
/**
* Get number of loops.
*
* @throws DecoderException
*/
public function loops(): int
{
try {
$unpacked = unpack('v*', substr($this->firstBlock()->value(), 1));
} catch (StateException $e) {
throw new DecoderException(
'Failed to decode loop count of netscape extension',
previous: $e
);
}
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to calculate loop count');
}
return $unpacked[1];
}
/**
* Set number of loops.
*
* @throws InvalidArgumentException
*/
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 text(): 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,68 @@
<?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;
/**
* Get image descriptor.
*/
public function imageDescriptor(): ImageDescriptor
{
return $this->imageDescriptor;
}
/**
* Set image descriptor for current instance.
*/
public function setImageDescriptor(ImageDescriptor $descriptor): self
{
$this->imageDescriptor = $descriptor;
return $this;
}
/**
* Get image data.
*/
public function imageData(): ImageData
{
return $this->imageData;
}
/**
* Set image data for current instance.
*/
public function setImageData(ImageData $data): self
{
$this->imageData = $data;
return $this;
}
/**
* Get current color table or null of table based based image has none.
*/
public function colorTable(): ?ColorTable
{
return $this->colorTable;
}
/**
* Set color table.
*/
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";
}

233
vendor/intervention/gif/src/Builder.php vendored Normal file
View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
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\Exceptions\StreamException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
use Intervention\Gif\Exceptions\StateException;
use Intervention\Gif\Traits\CanHandleFiles;
class Builder
{
use CanHandleFiles;
/**
* Create new instance.
*/
public function __construct(protected GifDataStream $gif = new GifDataStream())
{
//
}
/**
* Create new canvas.
*
* @throws InvalidArgumentException
*/
public static function canvas(int $width, int $height): self
{
return (new self())->setSize($width, $height);
}
/**
* Get GifDataStream object we're currently building.
*/
public function gifDataStream(): GifDataStream
{
return $this->gif;
}
/**
* Set canvas size of gif.
*
* @throws InvalidArgumentException
*/
public function setSize(int $width, int $height): self
{
$this->gif->logicalScreenDescriptor()->setSize($width, $height);
return $this;
}
/**
* Set loop count.
*
* @throws StateException
* @throws InvalidArgumentException
*/
public function setLoops(int $loops): self
{
if ($loops < 0) {
throw new InvalidArgumentException('The loop count must be equal to or greater than 0');
}
if ($this->gif->frames() === []) {
throw new StateException('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->firstFrame()?->clearApplicationExtensions();
return $this;
}
// make sure a netscape extension is present to store the loop count
if ($this->gif->firstFrame()?->netscapeExtension() === null) {
$this->gif->firstFrame()?->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->firstFrame()?->netscapeExtension()?->setLoops($loops);
return $this;
}
/**
* Create new animation frame from given source which can be path to a file or GIF image data.
*
* @throws DecoderException
* @throws StreamException
* @throws InvalidArgumentException
*/
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->firstFrame()?->graphicControlExtension();
if ($control !== null && $control->transparentColorExistance()) {
$extension->setTransparentColorExistance();
$extension->setTransparentColorIndex(
$control->transparentColorIndex()
);
}
return $extension;
}
/**
* Build table based image object from given source.
*
* @throws DecoderException
*/
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->imageDescriptor()->setLocalColorTableExistance();
$globalColorTable = $source->globalColorTable();
if ($globalColorTable === null) {
throw new DecoderException(
'Failed to build table based image. Unable to find global color table in gif data stream',
);
}
$block->setColorTable($globalColorTable);
$block->imageDescriptor()->setLocalColorTableSorted(
$source->logicalScreenDescriptor()->globalColorTableSorted()
);
try {
$block->imageDescriptor()->setLocalColorTableSize(
$source->logicalScreenDescriptor()->globalColorTableSize()
);
$block->imageDescriptor()->setSize(
$source->logicalScreenDescriptor()->width(),
$source->logicalScreenDescriptor()->height()
);
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode image source',
previous: $e
);
}
// set position
$block->imageDescriptor()->setPosition($left, $top);
// set interlaced flag
$block->imageDescriptor()->setInterlaced($interlaced);
// add image data from source
$block->setImageData(
$source->firstFrame()?->imageData() ?: throw new DecoderException(
'Failed to build table based image. Unable to find image data',
)
);
return $block;
}
/**
* Encode the current build.
*
* @throws EncoderException
*/
public function encode(): string
{
return $this->gif->encode();
}
}

50
vendor/intervention/gif/src/Decoder.php vendored Normal file
View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\StreamException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
use Intervention\Gif\Traits\CanHandleFiles;
class Decoder
{
use CanHandleFiles;
/**
* Decode given input.
*
* @throws InvalidArgumentException
* @throws StreamException
* @throws DecoderException
*/
public static function decode(mixed $input): GifDataStream
{
$stream = match (true) {
self::isFilePath($input) => self::streamFromFilePath($input),
is_string($input) => self::streamFromData($input),
self::isStream($input) => $input,
default => throw new InvalidArgumentException(
'Decoder input must be either file path, stream resource or binary data'
)
};
$result = rewind($stream);
if ($result === false) {
throw new StreamException('Failed to rewind stream');
}
return GifDataStream::decode($stream);
}
/**
* Determine if input is stream resource.
*/
private static function isStream(mixed $input): bool
{
return is_resource($input) && get_resource_type($input) === 'stream';
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Exceptions\DecoderException;
abstract class AbstractDecoder
{
/**
* Decode current source.
*/
abstract public function decode(): AbstractEntity;
/**
* Create new instance.
*/
public function __construct(protected mixed $stream, protected ?int $length = null)
{
//
}
/**
* Set source to decode.
*/
public function setStream(mixed $stream): self
{
$this->stream = $stream;
return $this;
}
/**
* Read given number of bytes and move stream position.
*
* @throws DecoderException
*/
protected function nextBytesOrFail(int $length): string
{
if ($length < 1) {
throw new DecoderException('The length of the next byte chain must be at least one byte');
}
$bytes = fread($this->stream, $length);
if ($bytes === false || strlen($bytes) !== $length) {
throw new DecoderException('Unexpected end of file');
}
return $bytes;
}
/**
* Read given number of bytes and move stream position back to previous position.
*
* @throws DecoderException
*/
protected function viewNextBytesOrFail(int $length): string
{
$bytes = $this->nextBytesOrFail($length);
$this->moveStreamPosition($length * -1);
return $bytes;
}
/**
* Read next byte and move stream position back to previous position.
*
* @throws DecoderException
*/
protected function viewNextByteOrFail(): string
{
return $this->viewNextBytesOrFail(1);
}
/**
* Read all remaining bytes from stream.
*
* @throws DecoderException
*/
protected function remainingBytes(): string
{
$contents = stream_get_contents($this->stream);
if ($contents === false) {
throw new DecoderException('Failed to read remaining bytes from stream');
}
return $contents;
}
/**
* Get next byte in stream and move stream position.
*
* @throws DecoderException
*/
protected function nextByteOrFail(): string
{
return $this->nextBytesOrFail(1);
}
/**
* Move stream position by given offset.
*
* @throws DecoderException
*/
protected function moveStreamPosition(int $offset): self
{
$result = fseek($this->stream, $offset, SEEK_CUR);
if ($result !== 0) {
throw new DecoderException('Failed to move stream position by offset ' . $offset);
}
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('Failed to decode given bytes');
}
return $unpacked[1];
}
/**
* Set length.
*/
public function setLength(int $length): self
{
$this->length = $length;
return $this;
}
/**
* Get length.
*/
public function length(): ?int
{
return $this->length;
}
/**
* Get current stream position.
*
* @throws DecoderException
*/
public function position(): int
{
$position = ftell($this->stream);
if ($position === false) {
throw new DecoderException('Failed to read current position from stream');
}
return $position;
}
}

View File

@@ -0,0 +1,48 @@
<?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('Failed to decode packed 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->packedBits($byte)[$num];
}
/**
* Get packed bits.
*
* @throws DecoderException
*/
protected function packedBits(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,87 @@
<?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\InvalidArgumentException;
class ApplicationExtensionDecoder extends AbstractDecoder
{
/**
* Decode current source.
*
* @throws DecoderException
*/
public function decode(): ApplicationExtension
{
$result = new ApplicationExtension();
$this->nextByteOrFail(); // marker
$this->nextByteOrFail(); // label
$blocksize = $this->decodeBlockSize($this->nextByteOrFail());
$application = $this->nextBytesOrFail($blocksize);
if ($application === NetscapeApplicationExtension::IDENTIFIER . NetscapeApplicationExtension::AUTH_CODE) {
$result = new NetscapeApplicationExtension();
// skip length
$this->nextByteOrFail();
try {
$result->setBlocks([
new DataSubBlock($this->nextBytesOrFail(3))
]);
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode image data sub block of image data',
previous: $e
);
}
// skip terminator
$this->nextByteOrFail();
return $result;
}
$result->setApplication($application);
// decode data sub blocks
$blocksize = $this->decodeBlockSize($this->nextByteOrFail());
while ($blocksize > 0) {
try {
$result->addBlock(new DataSubBlock($this->nextBytesOrFail($blocksize)));
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode image data sub block of image data',
previous: $e
);
}
$blocksize = $this->decodeBlockSize($this->nextByteOrFail());
}
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('Failed to decode block size of application extension');
}
return intval($unpacked[1]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\Color;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class ColorDecoder extends AbstractDecoder
{
/**
* Decode current source to Color.
*
* @throws DecoderException
*/
public function decode(): Color
{
try {
return new Color(
$this->decodeColorValue($this->nextByteOrFail()),
$this->decodeColorValue($this->nextByteOrFail()),
$this->decodeColorValue($this->nextByteOrFail()),
);
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode color channel values',
previous: $e
);
}
}
/**
* Decode color 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('Failed to decode color value');
}
return $unpacked[1];
}
}

View File

@@ -0,0 +1,29 @@
<?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();
$length = $this->length() !== null ? $this->length() : 0;
for ($i = 0; $i < ($length / 3); $i++) {
$table->addColor(Color::decode($this->stream));
}
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->nextBytesOrFail(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->nextByteOrFail();
$size = $this->decodeBlocksize($byte);
if ($size > 0) {
$comments[] = $this->nextBytesOrFail($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('Failed to decode block size of comment extension');
}
return intval($unpacked[1]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class DataSubBlockDecoder extends AbstractDecoder
{
/**
* Decode current source.
*
* @throws DecoderException
*/
public function decode(): DataSubBlock
{
$char = $this->nextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode data sub block');
}
$size = (int) $unpacked[1];
try {
return new DataSubBlock($this->nextBytesOrFail($size));
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode image data sub block of image data',
previous: $e
);
}
}
}

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->stream),
AbstractExtension::MARKER . NetscapeApplicationExtension::LABEL
=> NetscapeApplicationExtension::decode($this->stream),
AbstractExtension::MARKER . ApplicationExtension::LABEL
=> ApplicationExtension::decode($this->stream),
AbstractExtension::MARKER . PlainTextExtension::LABEL
=> PlainTextExtension::decode($this->stream),
AbstractExtension::MARKER . CommentExtension::LABEL
=> CommentExtension::decode($this->stream),
default => match ($this->viewNextByteOrFail()) {
ImageDescriptor::SEPARATOR => TableBasedImage::decode($this->stream),
default => throw new DecoderException('Failed 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->stream),
);
$gif->setLogicalScreenDescriptor(
LogicalScreenDescriptor::decode($this->stream),
);
if ($gif->logicalScreenDescriptor()->hasGlobalColorTable()) {
$length = $gif->logicalScreenDescriptor()->globalColorTableByteSize();
$gif->setGlobalColorTable(
ColorTable::decode($this->stream, $length)
);
}
while ($this->viewNextByteOrFail() !== Trailer::MARKER) {
match ($this->viewNextBytesOrFail(2)) {
// handle trailing "global" comment blocks which are not part of "FrameBlock"
AbstractExtension::MARKER . CommentExtension::LABEL
=> $gif->addComment(
CommentExtension::decode($this->stream)
),
default => $gif->addFrame(
FrameBlock::decode($this->stream)
),
};
}
return $gif;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
<?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\InvalidArgumentException;
class ImageDataDecoder extends AbstractDecoder
{
/**
* Decode current source.
*
* @throws DecoderException
*/
public function decode(): ImageData
{
$data = new ImageData();
// LZW min. code size
$char = $this->nextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode lzw min. code size of image data');
}
$data->setLzwMinCodeSize(intval($unpacked[1]));
do {
// decode sub blocks
$char = $this->nextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode image data sub block of image data');
}
$size = intval($unpacked[1]);
if ($size > 0) {
try {
$data->addBlock(new DataSubBlock($this->nextBytesOrFail($size)));
} catch (InvalidArgumentException $e) {
throw new DecoderException(
'Failed to decode image data sub block of image data',
previous: $e
);
}
}
} while ($char !== AbstractEntity::TERMINATOR);
return $data;
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class ImageDescriptorDecoder extends AbstractPackedBitDecoder
{
/**
* Decode given string to current instance.
*
* @throws DecoderException
*/
public function decode(): ImageDescriptor
{
$descriptor = new ImageDescriptor();
$this->nextByteOrFail(); // skip separator
$descriptor->setPosition(
$this->decodeMultiByte($this->nextBytesOrFail(2)),
$this->decodeMultiByte($this->nextBytesOrFail(2))
);
try {
$descriptor->setSize(
$this->decodeMultiByte($this->nextBytesOrFail(2)),
$this->decodeMultiByte($this->nextBytesOrFail(2))
);
} catch (InvalidArgumentException $e) {
throw new DecoderException('Failed to decode image size of image descriptor', previous: $e);
}
$packedField = $this->nextByteOrFail();
$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
*/
private function decodeLocalColorTableExistance(string $byte): bool
{
return $this->hasPackedBit($byte, 0);
}
/**
* Decode local color table sort method.
*
* @throws DecoderException
*/
private function decodeLocalColorTableSorted(string $byte): bool
{
return $this->hasPackedBit($byte, 2);
}
/**
* Decode local color table size.
*
* @throws DecoderException
*/
private function decodeLocalColorTableSize(string $byte): int
{
return (int) bindec($this->packedBits($byte, 5, 3));
}
/**
* Decode interlaced flag.
*
* @throws DecoderException
*/
private function decodeInterlaced(string $byte): bool
{
return $this->hasPackedBit($byte, 1);
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Decoders;
use Intervention\Gif\Blocks\LogicalScreenDescriptor;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
class LogicalScreenDescriptorDecoder extends AbstractPackedBitDecoder
{
/**
* Decode given string to current instance.
*
* @throws DecoderException
*/
public function decode(): LogicalScreenDescriptor
{
$logicalScreenDescriptor = new LogicalScreenDescriptor();
// bytes 1-4
try {
$logicalScreenDescriptor->setSize(
$this->decodeWidth($this->nextBytesOrFail(2)),
$this->decodeHeight($this->nextBytesOrFail(2))
);
} catch (InvalidArgumentException $e) {
throw new DecoderException('Failed to decode image size of logical screen descriptor', previous: $e);
}
// byte 5
$packedField = $this->nextByteOrFail();
$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->nextByteOrFail())
);
// byte 7
$logicalScreenDescriptor->setPixelAspectRatio(
$this->decodePixelAspectRatio($this->nextByteOrFail())
);
return $logicalScreenDescriptor;
}
/**
* Decode width.
*
* @throws DecoderException
*/
private function decodeWidth(string $source): int
{
$unpacked = unpack('v*', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode width in logical screen descriptor');
}
return $unpacked[1];
}
/**
* Decode height.
*
* @throws DecoderException
*/
private function decodeHeight(string $source): int
{
$unpacked = unpack('v*', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode height in logical screen descriptor');
}
return $unpacked[1];
}
/**
* Decode existance of global color table.
*
* @throws DecoderException
*/
private function decodeGlobalColorTableExistance(string $byte): bool
{
return $this->hasPackedBit($byte, 0);
}
/**
* Decode color resolution in bits per pixel.
*
* @throws DecoderException
*/
private function decodeBitsPerPixel(string $byte): int
{
return intval(bindec($this->packedBits($byte, 1, 3))) + 1;
}
/**
* Decode global color table sorted status.
*
* @throws DecoderException
*/
private function decodeGlobalColorTableSorted(string $byte): bool
{
return $this->hasPackedBit($byte, 4);
}
/**
* Decode size of global color table.
*
* @throws DecoderException
*/
private function decodeGlobalColorTableSize(string $byte): int
{
return intval(bindec($this->packedBits($byte, 5, 3)));
}
/**
* Decode background color index.
*
* @throws DecoderException
*/
private function decodeBackgroundColorIndex(string $source): int
{
$unpacked = unpack('C', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode background color index in logical screen descriptor');
}
return $unpacked[1];
}
/**
* Decode pixel aspect ratio.
*
* @throws DecoderException
*/
private function decodePixelAspectRatio(string $source): int
{
$unpacked = unpack('C', $source);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode pixel aspect ratio in logical screen descriptor');
}
return $unpacked[1];
}
}

View File

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

View File

@@ -0,0 +1,75 @@
<?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->nextBytesOrFail(2);
// skip info block
$this->nextBytesOrFail($this->infoBlockSize());
// text blocks
$extension->setText($this->decodeTextBlocks());
return $extension;
}
/**
* Get number of bytes in header block.
*
* @throws DecoderException
*/
private function infoBlockSize(): int
{
$unpacked = unpack('C', $this->nextByteOrFail());
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode info block size of plain text extension');
}
return $unpacked[1];
}
/**
* Decode text sub blocks.
*
* @throws DecoderException
* @return array<string>
*/
private function decodeTextBlocks(): array
{
$blocks = [];
do {
$char = $this->nextByteOrFail();
$unpacked = unpack('C', $char);
if ($unpacked === false || !array_key_exists(1, $unpacked)) {
throw new DecoderException('Failed to decode text blocks in plain text extension');
}
$size = (int) $unpacked[1];
if ($size > 0) {
$blocks[] = $this->nextBytesOrFail($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->stream));
if ($block->imageDescriptor()->hasLocalColorTable()) {
$block->setColorTable(
ColorTable::decode(
$this->stream,
$block->imageDescriptor()->localColorTableByteSize()
)
);
}
$block->setImageData(
ImageData::decode($this->stream)
);
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 entity.
*/
abstract public function encode(): string;
/**
* Create new instance.
*/
public function __construct(protected mixed $entity)
{
//
}
}

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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', [
ApplicationExtension::MARKER,
ApplicationExtension::LABEL,
pack('C', $this->entity->blockSize()),
$this->entity->application(),
implode('', array_map(fn(DataSubBlock $block): string => $block->encode(), $this->entity->blocks())),
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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
$this->encodeColorValue($this->entity->red()),
$this->encodeColorValue($this->entity->green()),
$this->encodeColorValue($this->entity->blue()),
]);
}
/**
* Encode color value.
*/
private 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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', array_map(
fn(Color $color): string => $color->encode(),
$this->entity->colors(),
));
}
}

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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
CommentExtension::MARKER,
CommentExtension::LABEL,
$this->encodeComments(),
CommentExtension::TERMINATOR,
]);
}
/**
* Encode comment blocks.
*/
private function encodeComments(): string
{
return implode('', array_map(function (string $comment): string {
return pack('C', strlen($comment)) . $comment;
}, $this->entity->comments()));
}
}

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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return pack('C', $this->entity->size()) . $this->entity->value();
}
}

View File

@@ -0,0 +1,49 @@
<?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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*
* @throws EncoderException
*/
public function encode(): string
{
$graphicControlExtension = $this->entity->graphicControlExtension();
$colorTable = $this->entity->colorTable();
$plainTextExtension = $this->entity->plainTextExtension();
return implode('', [
implode('', array_map(
fn(ApplicationExtension $extension): string => $extension->encode(),
$this->entity->applicationExtensions(),
)),
implode('', array_map(
fn(CommentExtension $extension): string => $extension->encode(),
$this->entity->commentExtensions(),
)),
$plainTextExtension ? $plainTextExtension->encode() : '',
$graphicControlExtension ? $graphicControlExtension->encode() : '',
$this->entity->imageDescriptor()->encode(),
$colorTable ? $colorTable->encode() : '',
$this->entity->imageData()->encode(),
]);
}
}

View File

@@ -0,0 +1,76 @@
<?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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', [
$this->entity->header()->encode(),
$this->entity->logicalScreenDescriptor()->encode(),
$this->maybeEncodeGlobalColorTable(),
$this->encodeFrames(),
$this->encodeComments(),
$this->entity->trailer()->encode(),
]);
}
/**
* Decode global color table if present in gif data.
*/
private function maybeEncodeGlobalColorTable(): string
{
if (!$this->entity->hasGlobalColorTable()) {
return '';
}
return $this->entity->globalColorTable()->encode();
}
/**
* Encode data blocks of source.
*
* @throws EncoderException
*/
private function encodeFrames(): string
{
return implode('', array_map(
fn(FrameBlock $frame): string => $frame->encode(),
$this->entity->frames(),
));
}
/**
* Encode comment extension blocks of source.
*
* @throws EncoderException
*/
private function encodeComments(): string
{
return implode('', array_map(
fn(CommentExtension $commentExtension): string => $commentExtension->encode(),
$this->entity->comments()
));
}
}

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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
GraphicControlExtension::MARKER,
GraphicControlExtension::LABEL,
GraphicControlExtension::BLOCKSIZE,
$this->encodePackedField(),
$this->encodeDelay(),
$this->encodeTransparentColorIndex(),
GraphicControlExtension::TERMINATOR,
]);
}
/**
* Encode delay time.
*/
private function encodeDelay(): string
{
return pack('v*', $this->entity->delay());
}
/**
* Encode transparent color index.
*/
private function encodeTransparentColorIndex(): string
{
return pack('C', $this->entity->transparentColorIndex());
}
/**
* Encode packed field.
*/
private function encodePackedField(): string
{
return pack('C', bindec(implode('', [
str_pad('0', 3, '0', STR_PAD_LEFT),
str_pad(decbin($this->entity->disposalMethod()->value), 3, '0', STR_PAD_LEFT),
(int) $this->entity->userInput(),
(int) $this->entity->transparentColorExistance(),
])));
}
}

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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return Header::SIGNATURE . $this->entity->version();
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\AbstractEntity;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Blocks\ImageData;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Exceptions\StateException;
class ImageDataEncoder extends AbstractEncoder
{
/**
* Create new instance.
*/
public function __construct(ImageData $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*
* @throws EncoderException
* @throws StateException
*/
public function encode(): string
{
if (!$this->entity->hasBlocks()) {
throw new StateException('No data blocks in image data');
}
return implode('', [
pack('C', $this->entity->lzwMinCodeSize()),
implode('', array_map(
fn(DataSubBlock $block): string => $block->encode(),
$this->entity->blocks(),
)),
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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
ImageDescriptor::SEPARATOR,
$this->encodeLeft(),
$this->encodeTop(),
$this->encodeWidth(),
$this->encodeHeight(),
$this->encodePackedField(),
]);
}
/**
* Encode left value.
*/
private function encodeLeft(): string
{
return pack('v*', $this->entity->left());
}
/**
* Encode top value.
*/
private function encodeTop(): string
{
return pack('v*', $this->entity->top());
}
/**
* Encode width value.
*/
private function encodeWidth(): string
{
return pack('v*', $this->entity->width());
}
/**
* Encode height value.
*/
private function encodeHeight(): string
{
return pack('v*', $this->entity->height());
}
/**
* Encode size of local color table.
*/
private function encodeLocalColorTableSize(): string
{
return str_pad(decbin($this->entity->localColorTableSize()), 3, '0', STR_PAD_LEFT);
}
/**
* Encode reserved field.
*/
private function encodeReservedField(): string
{
return str_pad('0', 2, '0', STR_PAD_LEFT);
}
/**
* Encode packed field.
*/
private function encodePackedField(): string
{
return pack('C', bindec(implode('', [
(int) $this->entity->localColorTableExistance(),
(int) $this->entity->isInterlaced(),
(int) $this->entity->localColorTableSorted(),
$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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
$this->encodeWidth(),
$this->encodeHeight(),
$this->encodePackedField(),
$this->encodeBackgroundColorIndex(),
$this->encodePixelAspectRatio(),
]);
}
/**
* Encode width of current instance.
*/
private function encodeWidth(): string
{
return pack('v*', $this->entity->width());
}
/**
* Encode height of current instance.
*/
private function encodeHeight(): string
{
return pack('v*', $this->entity->height());
}
/**
* Encode background color index of global color table.
*/
private function encodeBackgroundColorIndex(): string
{
return pack('C', $this->entity->backgroundColorIndex());
}
/**
* Encode pixel aspect ratio.
*/
private function encodePixelAspectRatio(): string
{
return pack('C', $this->entity->pixelAspectRatio());
}
/**
* Return color resolution for encoding.
*/
private function encodeColorResolution(): string
{
return str_pad(decbin($this->entity->bitsPerPixel() - 1), 3, '0', STR_PAD_LEFT);
}
/**
* Encode size of global color table.
*/
private function encodeGlobalColorTableSize(): string
{
return str_pad(decbin($this->entity->globalColorTableSize()), 3, '0', STR_PAD_LEFT);
}
/**
* Encode packed field of current instance.
*/
private function encodePackedField(): string
{
return pack('C', bindec(implode('', [
(int) $this->entity->globalColorTableExistance(),
$this->encodeColorResolution(),
(int) $this->entity->globalColorTableSorted(),
$this->encodeGlobalColorTableSize(),
])));
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\ApplicationExtension;
use Intervention\Gif\Blocks\DataSubBlock;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Exceptions\EncoderException;
class NetscapeApplicationExtensionEncoder extends ApplicationExtensionEncoder
{
/**
* Create new decoder instance.
*/
public function __construct(NetscapeApplicationExtension $entity)
{
parent::__construct($entity);
}
/**
* Encode current source.
*
* @throws EncoderException
*/
public function encode(): string
{
return implode('', [
ApplicationExtension::MARKER,
ApplicationExtension::LABEL,
pack('C', $this->entity->blockSize()),
$this->entity->application(),
implode('', array_map(fn(DataSubBlock $block): string => $block->encode(), $this->entity->blocks())),
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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
if (!$this->entity->hasText()) {
return '';
}
return implode('', [
PlainTextExtension::MARKER,
PlainTextExtension::LABEL,
$this->encodeHead(),
$this->encodeTexts(),
PlainTextExtension::TERMINATOR,
]);
}
/**
* Encode head block.
*/
private function encodeHead(): string
{
return "\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
}
/**
* Encode text chunks.
*/
private function encodeTexts(): string
{
return implode('', array_map(
fn(string $text): string => pack('C', strlen($text)) . $text,
$this->entity->text(),
));
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Encoders;
use Intervention\Gif\Blocks\TableBasedImage;
class TableBasedImageEncoder extends AbstractEncoder
{
/**
* Create new instance.
*/
public function __construct(TableBasedImage $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return implode('', [
$this->entity->imageDescriptor()->encode(),
$this->entity->colorTable() ? $this->entity->colorTable()->encode() : '',
$this->entity->imageData()->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 $entity)
{
parent::__construct($entity);
}
/**
* Encode current entity.
*/
public function encode(): string
{
return Trailer::MARKER;
}
}

View File

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

View File

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

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 FilesystemException extends RuntimeException
{
//
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,197 @@
<?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 header(): Header
{
return $this->header;
}
/**
* Set header.
*/
public function setHeader(Header $header): self
{
$this->header = $header;
return $this;
}
/**
* Get logical screen descriptor.
*/
public function logicalScreenDescriptor(): 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 globalColorTable(): ?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->logicalSize()
);
return $this;
}
/**
* Get main graphic control extension.
*/
public function mainApplicationExtension(): ?NetscapeApplicationExtension
{
foreach ($this->frames as $frame) {
$extension = $frame->netscapeExtension();
if ($extension !== null) {
return $extension;
}
}
return null;
}
/**
* Get array of frames.
*
* @return array<FrameBlock>
*/
public function frames(): array
{
return $this->frames;
}
/**
* Return array of "global" comments.
*
* @return array<CommentExtension>
*/
public function comments(): array
{
return $this->comments;
}
/**
* Return first frame.
*/
public function firstFrame(): ?FrameBlock
{
if ($this->frames === []) {
return null;
}
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 trailer(): Trailer
{
return new Trailer();
}
/**
* Determine if gif is animated.
*/
public function isAnimated(): bool
{
return count($this->frames()) > 1;
}
/**
* Determine if global color table is set.
*/
public function hasGlobalColorTable(): bool
{
return !is_null($this->globalColorTable);
}
}

394
vendor/intervention/gif/src/Splitter.php vendored Normal file
View File

@@ -0,0 +1,394 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif;
use ArrayIterator;
use GdImage;
use Intervention\Gif\Exceptions\CoreException;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Exceptions\StreamException;
use Intervention\Gif\Exceptions\InvalidArgumentException;
use Intervention\Gif\Exceptions\SplitterException;
use IteratorAggregate;
use Traversable;
/**
* @implements IteratorAggregate<GifDataStream|GdImage>
*/
class Splitter implements IteratorAggregate
{
/**
* Single frames resolved from main GifDataStream.
*
* @var array<GifDataStream|GdImage>
*/
protected array $frames = [];
/**
* Delays of each frame resolved from main GifDataStream.
*
* @var array<int>
*/
protected array $delays = [];
/**
* Loop count of main GifDataStream.
*/
protected int $loops;
/**
* Create new instance.
*
* @throws SplitterException
*/
public function __construct(protected GifDataStream $gif)
{
try {
$this->loops = $gif->mainApplicationExtension()?->loops() ?: 0;
} catch (DecoderException $e) {
throw new SplitterException('Failed to create instance from ' . GifDataStream::class, previous: $e);
}
}
/**
* Create splitter instance from gif data stream object.
*
* @throws SplitterException
*/
public static function create(GifDataStream $stream): self
{
return new self($stream);
}
/**
* Create splitter instance from raw binary gif image data.
*
* @throws SplitterException
* @throws InvalidArgumentException
* @throws StreamException
* @throws DecoderException
*/
public static function decode(mixed $input): self
{
return new self(Decoder::decode($input));
}
/**
* Iterate over the frames and pass each frame to a closure.
*/
public function each(callable $callback): self
{
array_map($callback, $this->frames, $this->delays);
return $this;
}
/**
* Set stream of instance.
*/
public function setStream(GifDataStream $stream): self
{
$this->gif = $stream;
return $this;
}
/**
* Build iterator.
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->frames);
}
/**
* Get frames.
*
* @return array<GifDataStream|GdImage>
*/
public function frames(): array
{
return $this->frames;
}
/**
* Get delays.
*
* @return array<int>
*/
public function delays(): array
{
return $this->delays;
}
/**
* Get loop count of currently handled gif data.
*/
public function loops(): int
{
return $this->loops;
}
/**
* Split current stream into array of seperate gif data stream objects for each frame.
*
* @throws SplitterException
*/
public function split(): self
{
$this->frames = [];
foreach ($this->gif->frames() as $frame) {
// create separate stream for each frame
try {
$gif = Builder::canvas(
$this->gif->logicalScreenDescriptor()->width(),
$this->gif->logicalScreenDescriptor()->height()
)->gifDataStream();
} catch (InvalidArgumentException $e) {
throw new SplitterException('Failed to create separate stream resource for each frame', previous: $e);
}
// check if working stream has global color table
$table = $this->gif->globalColorTable();
if ($table !== null) {
$gif->setGlobalColorTable($table);
$gif->logicalScreenDescriptor()->setGlobalColorTableExistance(true);
$gif->logicalScreenDescriptor()->setGlobalColorTableSorted(
$this->gif->logicalScreenDescriptor()->globalColorTableSorted()
);
$gif->logicalScreenDescriptor()->setGlobalColorTableSize(
$this->gif->logicalScreenDescriptor()->globalColorTableSize()
);
$gif->logicalScreenDescriptor()->setBackgroundColorIndex(
$this->gif->logicalScreenDescriptor()->backgroundColorIndex()
);
$gif->logicalScreenDescriptor()->setPixelAspectRatio(
$this->gif->logicalScreenDescriptor()->pixelAspectRatio()
);
$gif->logicalScreenDescriptor()->setBitsPerPixel(
$this->gif->logicalScreenDescriptor()->bitsPerPixel()
);
}
// copy original frame
$gif->addFrame($frame);
$this->frames[] = $gif;
$this->delays[] = match (is_object($frame->graphicControlExtension())) {
true => $frame->graphicControlExtension()->delay(),
default => 0,
};
}
return $this;
}
/**
* Transform current frames to an a rray of transparency flattened GdImage objects for each frame.
*
* @throws SplitterException
* @throws CoreException
*/
public function flatten(): self
{
$frames = $this->unprocessedFramesOrFail();
$gdImages = $this->extractFrames();
// non-animated gif files don't need to be flattened
// just replace frames with extracted
if (count($frames) === 1) {
$this->frames = $gdImages;
return $this;
}
// get main image size
$width = imagesx($gdImages[0]);
$height = imagesy($gdImages[0]);
$transparent = imagecolortransparent($gdImages[0]);
foreach ($gdImages as $key => $gdImage) {
// get meta data of frame
$gif = $frames[$key];
$descriptor = $gif->firstFrame()?->imageDescriptor();
$offsetX = $descriptor?->left() ?: 0;
$offsetY = $descriptor?->top() ?: 0;
$w = $descriptor?->width() ?: 0;
$h = $descriptor?->height() ?: 0;
if (in_array($this->disposalMethod($gif), [DisposalMethod::NONE, DisposalMethod::PREVIOUS])) {
if ($key >= 1) {
// create normalized gd image
$canvas = imagecreatetruecolor($width, $height);
if ($canvas === false) {
throw new CoreException('Failed to create new image instance for animation frame #' . $key);
}
if (imagecolortransparent($gdImage) !== -1) {
$transparent = imagecolortransparent($gdImage);
} else {
$transparent = imagecolorallocatealpha($gdImage, 255, 0, 255, 127);
}
if (!is_int($transparent)) {
throw new CoreException(
'Failed to allocate transparent color in animation frame #' . $key,
);
}
// fill with transparent
imagefill($canvas, 0, 0, $transparent);
imagecolortransparent($canvas, $transparent);
imagealphablending($canvas, true);
// insert last as base
imagecopy(
$canvas,
$gdImages[$key - 1],
0,
0,
0,
0,
$width,
$height
);
// insert gd image
imagecopy(
$canvas,
$gdImage,
$offsetX,
$offsetY,
0,
0,
$w,
$h
);
} else {
imagealphablending($gdImage, true);
$canvas = $gdImage;
}
} else {
// create normalized gd image
$canvas = imagecreatetruecolor($width, $height);
if ($canvas === false) {
throw new CoreException('Failed to create new image instance for animation frame #' . $key);
}
if (imagecolortransparent($gdImage) !== -1) {
$transparent = imagecolortransparent($gdImage);
} else {
$transparent = imagecolorallocatealpha($gdImage, 255, 0, 255, 127);
}
if (!is_int($transparent)) {
throw new CoreException('Animation frames cannot be converted into GdImage objects');
}
// fill with transparent
imagefill($canvas, 0, 0, $transparent);
imagecolortransparent($canvas, $transparent);
imagealphablending($canvas, true);
// insert frame gd image
imagecopy(
$canvas,
$gdImage,
$offsetX,
$offsetY,
0,
0,
$w,
$h
);
}
$gdImages[$key] = $canvas;
}
$this->frames = $gdImages;
return $this;
}
/**
* Return array of GdImage objects for each frame.
*
* @throws CoreException
* @throws SplitterException
* @return array<GdImage>
*/
private function extractFrames(): array
{
$gdImages = [];
foreach ($this->unprocessedFramesOrFail() as $frame) {
try {
$gdImage = imagecreatefromstring($frame->encode());
} catch (EncoderException) {
throw new CoreException('Failed to extract animation frame to GdImage object');
}
if ($gdImage === false) {
throw new CoreException('Failed to extract animation frame to GdImage object');
}
imagepalettetotruecolor($gdImage);
imagesavealpha($gdImage, true);
$gdImages[] = $gdImage;
}
return $gdImages;
}
/**
* Find and return disposal method of given gif data stream.
*
* @throws SplitterException
*/
private function disposalMethod(GifDataStream $gif): DisposalMethod
{
$disposalMethod = $gif->firstFrame()?->graphicControlExtension()?->disposalMethod();
return $disposalMethod ?: throw new SplitterException('Failed to find disposal method in gif data stream');
}
/**
* Return array of unprocessed frames or throw exception if frames are already processed.
*
* @throws SplitterException
* @return array<GifDataStream>
*/
private function unprocessedFramesOrFail(): array
{
if (count($this->frames) === 0) {
throw new SplitterException('No frames available. Run ' . $this::class . '::split() first');
}
// if any frame is instanceof GdImage, frame was already flattened
$processed = count(array_filter(
$this->frames,
fn(GdImage|GifDataStream $frame): bool => $frame instanceof GdImage,
)) > 0;
if ($processed) {
throw new SplitterException('Frames have already been flattened');
}
return array_filter(
$this->frames,
fn(GifDataStream|GdImage $frame): bool => $frame instanceof GifDataStream,
);
}
}

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::decoder($source, $length)->decode();
}
/**
* Get decoder for current instance.
*
* @throws DecoderException
*/
protected static function decoder(mixed $source, ?int $length = null): AbstractDecoder
{
$classname = sprintf('Intervention\Gif\Decoders\%sDecoder', self::shortClassname());
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->encoder()->encode();
}
/**
* Get encoder object for current entity.
*
* @throws EncoderException
*/
protected function encoder(): AbstractEncoder
{
$classname = sprintf('Intervention\Gif\Encoders\%sEncoder', self::shortClassname());
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,70 @@
<?php
declare(strict_types=1);
namespace Intervention\Gif\Traits;
use Intervention\Gif\Exceptions\StreamException;
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) === true;
}
/**
* Determine if given string contains null bytes.
*/
private static function hasNullBytes(string $string): bool
{
return str_contains($string, chr(0));
}
/**
* Create stream resource from given gif image data.
*
* @throws StreamException
*/
private static function streamFromData(string $data): mixed
{
$stream = fopen('php://temp', 'r+');
if ($stream === false) {
throw new StreamException('Failed to create tempory stream resource');
}
$result = fwrite($stream, $data);
if ($result === false) {
fclose($stream);
throw new StreamException('Failed to write tempory stream resource');
}
$result = rewind($stream);
if ($result === false) {
fclose($stream);
throw new StreamException('Failed to rewind tempory stream resource');
}
return $stream;
}
/**
* Create stream resource from given file path.
*
* @throws StreamException
*/
private static function streamFromFilePath(string $path): mixed
{
$stream = fopen($path, 'rb');
if ($stream === false) {
throw new StreamException('Failed to create stream resource from path');
}
return $stream;
}
}

21
vendor/intervention/image/LICENSE vendored Normal file
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.

51
vendor/intervention/image/composer.json vendored Normal file
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.3",
"ext-mbstring": "*",
"intervention/gif": "^5"
},
"require-dev": {
"phpunit/phpunit": "^12.0",
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"squizlabs/php_codesniffer": "^4",
"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
}
}
}

90
vendor/intervention/image/readme.md vendored Normal file
View File

@@ -0,0 +1,90 @@
# 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 popular PHP image manipulation extensions. You can
choose between the GD library, Imagick or libvips as the base layer for all operations.
- Fluent interface for common image editing tasks
- Interchangeable driver architecture with support for **GD, Imagick and libvips**
- Support for animated images with all drivers
- Framework-agnostic
## Installation
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/v4/basics/instantiation/) on
how to use Intervention Image and more with the [official documentation](https://image.intervention.io/v4/).
## Code Examples
```php
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Alignment;
use Intervention\Image\Color;
use Intervention\Image\Format;
// create image manager instance using the preferred driver
$manager = ImageManager::usingDriver(GdDriver::class);
// read image data from path
$image = $manager->decodePath('images/example.webp');
// scale image by height
$image->scale(height: 300);
// insert a watermark
$image->insert('images/watermark.png', alignment: Alignment::BOTTOM_RIGHT);
// encode edited image
$encoded = $image->encodeUsingFormat(Format::JPEG, quality: 65);
// 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.3
- Mbstring PHP Extension
- Image Processing PHP Extension (GD, Imagick or libvips)
## 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,238 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Error;
use Intervention\Image\Exceptions\InvalidArgumentException;
enum Alignment: string
{
case TOP = 'top';
case TOP_RIGHT = 'top-right';
case RIGHT = 'right';
case BOTTOM_RIGHT = 'bottom-right';
case BOTTOM = 'bottom';
case BOTTOM_LEFT = 'bottom-left';
case LEFT = 'left';
case TOP_LEFT = 'top-left';
case CENTER = 'center';
/**
* Create position from given identifier.
*
* @throws InvalidArgumentException
*/
public static function create(string|self $identifier): self
{
if ($identifier instanceof self) {
return $identifier;
}
try {
$position = self::from(strtolower($identifier));
} catch (Error) {
$position = match (strtolower($identifier)) {
'top-center',
'top_center',
'topcenter',
'center-top',
'center_top',
'centertop',
'top-middle',
'top_middle',
'topmiddle',
'middle-top',
'middle_top',
'middletop' => self::TOP,
'top_right',
'topright',
'right-top',
'right_top',
'righttop' => self::TOP_RIGHT,
'right-center',
'right_center',
'rightcenter',
'center-right',
'center_right',
'centerright',
'right-middle',
'right_middle',
'rightmiddle',
'middle-right',
'middle_right',
'middleright' => self::RIGHT,
'bottom_right',
'bottomright',
'right-bottom',
'right_bottom',
'rightbottom' => self::BOTTOM_RIGHT,
'bottom-center',
'bottom_center',
'bottomcenter',
'center-bottom',
'center_bottom',
'centerbottom',
'bottom-middle',
'bottom_middle',
'bottommiddle',
'middle-bottom',
'middle_bottom',
'middlebottom' => self::BOTTOM,
'bottom_left',
'bottomleft',
'left-bottom',
'left_bottom',
'leftbottom' => self::BOTTOM_LEFT,
'left-center',
'left_center',
'leftcenter',
'center-left',
'center_left',
'centerleft',
'left-middle',
'left_middle',
'leftmiddle',
'middle-left',
'middle_left',
'middleleft' => self::LEFT,
'top_left',
'topleft',
'left-top',
'left_top',
'lefttop' => self::TOP_LEFT,
'middle',
'center-center',
'center_center',
'centercenter',
'center-middle',
'center_middle',
'centermiddle',
'middle-center',
'middle_center',
'middlecenter' => self::CENTER,
default => throw new InvalidArgumentException(
'Unable to create ' . self::class . ' from "' . $identifier . '"',
),
};
}
return $position;
}
/**
* Try to create position from given identifier or return null on failure.
*/
public static function tryCreate(string|self $identifier): ?self
{
try {
return self::create($identifier);
} catch (InvalidArgumentException) {
return null;
}
}
/**
* Change the current alignment by adjusting only the horizontal axis to the specified value.
*/
public function alignHorizontally(string|self $alignment): self
{
// handle "leftish" alignments
if (in_array($alignment, [self::LEFT, self::BOTTOM_LEFT, self::TOP_LEFT])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_LEFT,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_LEFT,
self::CENTER, self::LEFT, self::RIGHT => self::LEFT,
};
}
// handle "rightish" alignments
if (in_array($alignment, [self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_RIGHT,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_RIGHT,
self::CENTER, self::LEFT, self::RIGHT => self::RIGHT,
};
}
// handle centering
if (in_array($alignment, [self::CENTER, self::TOP, self::BOTTOM])) {
return match ($this) {
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM,
self::CENTER, self::LEFT, self::RIGHT => self::CENTER,
};
}
return $this;
}
/**
* Change the current alignment by adjusting only the vertical axis to the specified value.
*/
public function alignVertically(string|self $alignment): self
{
// handle "bottomish" alignments
if (in_array($alignment, [self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::BOTTOM_LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::BOTTOM_RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::BOTTOM,
};
}
// handle "topish" alignments
if (in_array($alignment, [self::TOP, self::TOP_RIGHT, self::TOP_LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::TOP_LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::TOP_RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::TOP,
};
}
// handle centering
if (in_array($alignment, [self::CENTER, self::RIGHT, self::LEFT])) {
return match ($this) {
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT,
self::CENTER, self::TOP, self::BOTTOM => self::CENTER,
};
}
return $this;
}
/**
* Return only the horizontal alignment.
*/
public function horizontal(): self
{
return match ($this) {
self::TOP, self::CENTER, self::BOTTOM => self::CENTER,
self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT,
self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT,
};
}
/**
* Return only the vertical alignment.
*/
public function vertical(): self
{
return match ($this) {
self::CENTER, self::RIGHT, self::LEFT => self::CENTER,
self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP,
self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM,
};
}
}

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 = 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,174 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Error;
use Intervention\Gif\DisposalMethod;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Exceptions\FilesystemException;
use Intervention\Image\Interfaces\AnimationFactoryInterface;
use Intervention\Image\Interfaces\DriverInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
class AnimationFactory implements AnimationFactoryInterface
{
/**
* Current frame number.
*/
protected int $currentFrameNumber = 0;
/**
* Image sources of animation frames.
*
* @var array<mixed>
*/
protected array $sources = [];
/**
* Frame delays of animation frames in seconds.
*
* @var array<float>
*/
protected array $delays = [];
/**
* Frame processing call names.
*
* @var array<null|string>
*/
protected array $processingCalls = [];
/**
* Frame processing arguments of calls.
*
* @var array<null|array<mixed>>
*/
protected array $processingArguments = [];
/**
* Create new instance.
*/
public function __construct(
protected int $width,
protected int $height,
null|callable $animation = null,
) {
if (is_callable($animation)) {
$animation($this);
}
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::build()
*/
public static function build(
int $width,
int $height,
callable $animation,
DriverInterface $driver,
): ImageInterface {
return (new self($width, $height, $animation))->image($driver);
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::add()
*/
public function add(mixed $source, float $delay = 1): AnimationFactoryInterface
{
$this->currentFrameNumber++;
$this->sources[$this->currentFrameNumber] = $source;
$this->delays[$this->currentFrameNumber] = $delay;
$this->processingCalls[$this->currentFrameNumber] = null;
$this->processingArguments[$this->currentFrameNumber] = null;
return $this;
}
/**
* {@inheritdoc}
*
* @see AnimationFactoryInterface::image()
*/
public function image(DriverInterface $driver): ImageInterface
{
if (count($this->sources) === 0) {
return $driver->createImage($this->width, $this->height);
}
$frames = array_map(
$this->buildFrame(...),
array_fill(0, count($this->sources), $driver),
$this->sources,
$this->delays,
$this->processingCalls,
$this->processingArguments,
);
return new Image($driver, $driver->createCore($frames));
}
/**
* Build frame from given image source and delay.
*
* @param null|array<mixed> $processingArguments
*/
private function buildFrame(
DriverInterface $driver,
mixed $source,
float $delay,
?string $processingCall = null,
?array $processingArguments = null,
): FrameInterface {
try {
// try to decode image source
$image = $driver->decodeImage($source);
} catch (DecoderException | FilesystemException) {
// create empty image with colored background
$image = $driver->createImage($this->width, $this->height)
->fill($driver->decodeColor($source));
}
// adjust size if necessary
if ($image->width() !== $this->width || $image->height() !== $this->height) {
$image->cover($this->width, $this->height);
}
// apply processing call if available
if ($processingCall !== null) {
call_user_func_array([$image, $processingCall], $processingArguments);
}
// return ready-made frame with all attributes
return $image
->core()
->first()
->setDelay($delay)
->setDisposalMethod(DisposalMethod::BACKGROUND->value);
}
/**
* Collect processing calls on frame images.
*
* @param array<null|array<mixed>> $arguments
* @throws Error
*/
public function __call(string $name, array $arguments): self
{
if (!method_exists(Image::class, $name)) {
throw new Error('Call to undefined method ' . Image::class . '::' . $name . '()');
}
$this->processingCalls[$this->currentFrameNumber] = $name;
$this->processingArguments[$this->currentFrameNumber] = $arguments;
return $this;
}
}

View File

@@ -0,0 +1,234 @@
<?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
*/
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);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::set()
*/
public function set(int|string $key, mixed $item): self
{
$this->items[$key] = $item;
return $this;
}
/**
* 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.
*
* @return int<0, max>
*/
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 (count($this->items) === 0) {
return null;
}
return reset($this->items);
}
/**
* Returns last item in collection.
*/
public function last(): mixed
{
if (count($this->items) === 0) {
return null;
}
return end($this->items);
}
/**
* Return item at given position starting at 0.
*/
public function at(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;
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::map()
*/
public function map(callable $callback): self
{
return new self(
array_map(
fn(mixed $item) => $callback($item),
$this->items,
)
);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::map()
*/
public function filter(callable $callback): self
{
return new self(
array_filter(
$this->items,
fn(mixed $item) => $callback($item),
)
);
}
/**
* {@inheritdoc}
*
* @see CollectionInterface::clear()
*/
public function clear(): 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;
}
}

173
vendor/intervention/image/src/Color.php vendored Normal file
View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Intervention\Image;
use Intervention\Image\Colors\Cmyk\Channels\Cyan;
use Intervention\Image\Colors\Cmyk\Channels\Key;
use Intervention\Image\Colors\Cmyk\Channels\Magenta;
use Intervention\Image\Colors\Cmyk\Channels\Yellow;
use Intervention\Image\Colors\Rgb\Color as RgbColor;
use Intervention\Image\Colors\Cmyk\Color as CmykColor;
use Intervention\Image\Colors\Hsl\Color as HslColor;
use Intervention\Image\Colors\Hsv\Color as HsvColor;
use Intervention\Image\Colors\Oklab\Color as OklabColor;
use Intervention\Image\Colors\Oklch\Color as OklchColor;
use Intervention\Image\Exceptions\DriverException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder as CmykStringColorDecoder;
use Intervention\Image\Colors\Hsl\Decoders\StringColorDecoder as HslStringColorDecoder;
use Intervention\Image\Colors\Hsv\Decoders\StringColorDecoder as HsvStringColorDecoder;
use Intervention\Image\Colors\Oklab\Decoders\StringColorDecoder as OklabStringColorDecoder;
use Intervention\Image\Colors\Oklch\Decoders\StringColorDecoder as OklchStringColorDecoder;
use Intervention\Image\Colors\Rgb\Channels\Alpha as RgbAlpha;
use Intervention\Image\Colors\Cmyk\Channels\Alpha as CmykAlpha;
use Intervention\Image\Colors\Hsl\Channels\Alpha as HslAlpha;
use Intervention\Image\Colors\Hsl\Channels\Hue as HslHue;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation as HslSaturation;
use Intervention\Image\Colors\Hsv\Channels\Alpha as HsvAlpha;
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\Oklab\Channels\A;
use Intervention\Image\Colors\Oklab\Channels\Alpha as OklabAlpha;
use Intervention\Image\Colors\Oklab\Channels\B;
use Intervention\Image\Colors\Oklab\Channels\Lightness as OklabLightness;
use Intervention\Image\Colors\Oklch\Channels\Alpha as OklchAlpha;
use Intervention\Image\Colors\Oklch\Channels\Chroma;
use Intervention\Image\Colors\Oklch\Channels\Lightness as OklchLightness;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Colors\Rgb\Decoders\HexColorDecoder as RgbHexColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\NamedColorDecoder;
use Intervention\Image\Colors\Rgb\Decoders\StringColorDecoder as RgbStringColorDecoder;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\NotSupportedException;
class Color
{
/**
* Parse color from string value.
*
* @throws InvalidArgumentException
* @throws ColorException
*/
public static function parse(string $input): ColorInterface
{
try {
$color = InputHandler::usingDecoders([
RgbStringColorDecoder::class,
CmykStringColorDecoder::class,
HsvStringColorDecoder::class,
HslStringColorDecoder::class,
OklabStringColorDecoder::class,
OklchStringColorDecoder::class,
NamedColorDecoder::class,
RgbHexColorDecoder::class,
])->handle($input);
} catch (NotSupportedException | DriverException $e) {
throw new InvalidArgumentException(
'Unable to parse RGB color from input "' . $input . '"',
previous: $e,
);
}
if (!$color instanceof ColorInterface) {
throw new ColorException('Result must be instance of ' . self::class . ', got ' . $color::class);
}
return $color;
}
/**
* Create new RGB color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function rgb(int|Red $r, int|Green $g, int|Blue $b, float|RgbAlpha $a = 1): RgbColor
{
return new RgbColor($r, $g, $b, $a);
}
/**
* Create new CMYK color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function cmyk(
int|Cyan $c,
int|Magenta $m,
int|Yellow $y,
int|Key $k,
float|CmykAlpha $a = 1,
): CmykColor {
return new CmykColor($c, $m, $y, $k, $a);
}
/**
* Create new HSL color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function hsl(int|HslHue $h, int|HslSaturation $s, int|Luminance $l, float|HslAlpha $a = 1): HslColor
{
return new HslColor($h, $s, $l, $a);
}
/**
* Create new HSV color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function hsv(int|Hue $h, int|Saturation $s, int|Value $v, float|HsvAlpha $a = 1): HsvColor
{
return new HsvColor($h, $s, $v, $a);
}
/**
* Create new OKLAB color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function oklab(
float|OklabLightness $l,
float|A $a,
float|B $b,
float|OklabAlpha $alpha = 1,
): OklabColor {
return new OklabColor($l, $a, $b, $alpha);
}
/**
* Create new OKLCH color.
*
* @throws InvalidArgumentException
* @throws DriverException
*/
public static function oklch(
float|OklchLightness $l,
float|Chroma $c,
float|Hue $h,
float|OklchAlpha $a = 1,
): OklchColor {
return new OklchColor($l, $c, $h, $a);
}
/**
* Create transparent RGB color.
*/
public static function transparent(): ColorInterface
{
// @phpstan-ignore missingType.checkedException
return new RgbColor(255, 255, 255, 0);
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation;
use Intervention\Image\Colors\Hsl\Colorspace as HslColorspace;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
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()
*
* @throws InvalidArgumentException
*/
public function channel(string $classname): ColorChannelInterface
{
$channels = array_filter(
$this->channels(),
fn(ColorChannelInterface $channel): bool => $channel::class === $classname,
);
if (count($channels) === 0) {
throw new InvalidArgumentException('Color channel ' . $classname . ' could not be found');
}
return reset($channels);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::toColorspace()
*
* @throws InvalidArgumentException
*/
public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface
{
if (is_string($colorspace) && !class_exists($colorspace)) {
throw new InvalidArgumentException('Unknown color space (' . $colorspace . ') as conversion target');
}
$colorspace = is_string($colorspace) ? new $colorspace() : $colorspace;
if (!$colorspace instanceof ColorspaceInterface) {
throw new InvalidArgumentException('Given color space must implement ' . ColorspaceInterface::class);
}
return $colorspace->importColor($this);
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isTransparent()
*/
public function isTransparent(): bool
{
return $this->alpha()->value() < $this->alpha()->max();
}
/**
* {@inheritdoc}
*
* @see ColorInterface::isClear()
*/
public function isClear(): bool
{
return floatval($this->alpha()->value()) === 0.0;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withTransparency()
*
* @throws InvalidArgumentException
*/
public function withTransparency(float $transparency): ColorInterface
{
$color = clone $this;
$color->channels = array_map(
fn(ColorChannelInterface $channel): ColorChannelInterface =>
$channel instanceof AlphaChannel ? $channel::fromNormalized($transparency) : $channel,
$this->channels
);
return $color;
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withBrightness()
*
* @throws InvalidArgumentException
*/
public function withBrightness(int $level): ColorInterface
{
$hsl = clone $this->toColorspace(HslColorspace::class);
$hsl->channel(Luminance::class)->scale($level);
return $hsl->toColorspace($this->colorspace());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withSaturation()
*
* @throws InvalidArgumentException
*/
public function withSaturation(int $level): ColorInterface
{
$hsl = clone $this->toColorspace(HslColorspace::class);
$hsl->channel(Saturation::class)->scale($level);
return $hsl->toColorspace($this->colorspace());
}
/**
* {@inheritdoc}
*
* @see ColorInterface::withInversion()
*
* @throws ColorException
*/
public function withInversion(): ColorInterface
{
try {
$rgb = $this->toColorspace(RgbColorspace::class);
} catch (InvalidArgumentException) {
throw new ColorException('Failed to invert color');
}
try {
$inverted = new \Intervention\Image\Colors\Rgb\Color(
255 - $rgb->channel(Red::class)->value(),
255 - $rgb->channel(Green::class)->value(),
255 - $rgb->channel(Blue::class)->value(),
$rgb->alpha()->normalized(),
);
return $inverted->toColorspace($this->colorspace());
} catch (InvalidArgumentException) {
throw new ColorException('Failed to invert color');
}
}
/**
* Show debug info for the current color.
*
* @return array<string, string>
*/
public function __debugInfo(): array
{
return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) {
$key = strtolower((new ReflectionClass($item))->getShortName());
$result[$key] = $item->toString();
return $result;
}, []);
}
/**
* Clone color.
*/
public function __clone(): void
{
foreach ($this->channels as $key => $channel) {
$this->channels[$key] = clone $channel;
}
}
/**
* {@inheritdoc}
*
* @see ColorInterface::__toString()
*/
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Stringable;
abstract class AbstractColorChannel implements ColorChannelInterface, Stringable
{
/**
* Main color channel value.
*/
protected int|float $value;
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::normalized()
*/
public function normalized(int $precision = 32): float
{
return round(($this->value() - $this->min()) / ($this->max() - $this->min()), $precision);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::scale()
*
* @throws InvalidArgumentException
*/
public function scale(int $percent): self
{
if ($percent === 0) {
return $this;
}
if ($percent < -100 || $percent > 100) {
throw new InvalidArgumentException('Percentage value must be between -100 and 100');
}
$normalized = $this->normalized();
$base = $percent >= 0 ? (1 - $normalized) : $normalized;
$scaled = min(1.0, max(0.0, $normalized + $base / 100 * $percent));
$this->value = static::fromNormalized($scaled)->value();
return $this;
}
/**
* Throw exception if the given value is not applicable for channel
* otherwise the value is returned unchanged.
*
* @throws InvalidArgumentException
*/
protected function validValueOrFail(int|float $value): mixed
{
if ($value < $this->min() || $value > $this->max()) {
throw new InvalidArgumentException(
'Color channel ' . $this::class . ' 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,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Interfaces\ColorspaceInterface;
abstract class AbstractColorspace implements ColorspaceInterface
{
/**
* Channel class names of colorspace.
*
* @var array<string>
*/
protected static array $channels = [];
/**
* {@inheritdoc}
*
* @see ColorspaceInterface::channels()
*/
public static function channels(): array
{
return static::$channels;
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors;
use Intervention\Image\Exceptions\InvalidArgumentException;
class AlphaChannel extends AbstractColorChannel
{
/**
* @throws InvalidArgumentException
*/
final public function __construct(float $value = 1)
{
if ($value < 0 || $value > 1) {
throw new InvalidArgumentException(
'Color channel value of ' . static::class . ' must be in range 0 to 1',
);
}
$this->value = (int) $this->validValueOrFail(intval(round($value * $this->max())));
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::fromNormalized()
*
* @throws InvalidArgumentException
*/
public static function fromNormalized(float $normalized): self
{
return new static($normalized);
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::value()
*/
public function value(): int
{
return $this->value;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
return 255;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::toString()
*/
public function toString(): string
{
return strval($this->normalized(2));
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\AlphaChannel;
class Alpha extends AlphaChannel
{
//
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Colors\Cmyk\Channels;
use Intervention\Image\Colors\IntegerColorChannel;
class Cyan extends IntegerColorChannel
{
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::min()
*/
public static function min(): float
{
return 0;
}
/**
* {@inheritdoc}
*
* @see ColorChannelInterface::max()
*/
public static function max(): float
{
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
{
//
}

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