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

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;
}
}