View file vendor/intervention/gif/src/Builder.php

File size: 5.77Kb
<?php

declare(strict_types=1);

namespace Intervention\Gif;

use Exception;
use Intervention\Gif\Blocks\FrameBlock;
use Intervention\Gif\Blocks\GraphicControlExtension;
use Intervention\Gif\Blocks\ImageDescriptor;
use Intervention\Gif\Blocks\NetscapeApplicationExtension;
use Intervention\Gif\Blocks\TableBasedImage;
use Intervention\Gif\Exceptions\DecoderException;
use Intervention\Gif\Exceptions\EncoderException;
use Intervention\Gif\Traits\CanHandleFiles;

class Builder
{
    use CanHandleFiles;

    /**
     * Create new instance
     */
    public function __construct(protected GifDataStream $gif = new GifDataStream())
    {
        //
    }

    /**
     * Create new canvas
     */
    public static function canvas(int $width, int $height): self
    {
        return (new self())->setSize($width, $height);
    }

    /**
     * Get GifDataStream object we're currently building
     */
    public function getGifDataStream(): GifDataStream
    {
        return $this->gif;
    }

    /**
     * Set canvas size of gif
     */
    public function setSize(int $width, int $height): self
    {
        $this->gif->getLogicalScreenDescriptor()->setSize($width, $height);

        return $this;
    }

    /**
     * Set loop count
     *
     * @throws Exception
     */
    public function setLoops(int $loops): self
    {
        if ($loops < 0) {
            throw new Exception('The loop count must be equal to or greater than 0');
        }

        if ($this->gif->getFrames() === []) {
            throw new Exception('Add at least one frame before setting the loop count');
        }

        // with one single loop the netscape extension must be removed otherwise the
        // gif is looped twice because the first repetition always takes place
        if ($loops === 1) {
            $this->gif->getFirstFrame()?->clearApplicationExtensions();
            return $this;
        }

        // make sure a netscape extension is present to store the loop count
        if (!$this->gif->getFirstFrame()?->getNetscapeExtension()) {
            $this->gif->getFirstFrame()?->addApplicationExtension(
                new NetscapeApplicationExtension()
            );
        }

        // the loop count is reduced by one because what is referred to here as
        // the “loop count” actually means repetitions in GIF format, and thus
        // the first repetition always takes place. A loop count of 0 howerver
        // means infinite repetitions and remains unaltered.
        $loops = $loops === 0 ? $loops : $loops - 1;

        // add loop count to netscape extension on first frame
        $this->gif->getFirstFrame()?->getNetscapeExtension()?->setLoops($loops);

        return $this;
    }

    /**
     * Create new animation frame from given source
     * which can be path to a file or GIF image data
     *
     * @throws DecoderException
     */
    public function addFrame(
        mixed $source,
        float $delay = 0,
        int $left = 0,
        int $top = 0,
        bool $interlaced = false
    ): self {
        $frame = new FrameBlock();
        $source = Decoder::decode($source);

        // store delay
        $frame->setGraphicControlExtension(
            $this->buildGraphicControlExtension(
                $source,
                intval($delay * 100)
            )
        );

        // store image
        $frame->setTableBasedImage(
            $this->buildTableBasedImage($source, $left, $top, $interlaced)
        );

        // add frame
        $this->gif->addFrame($frame);

        return $this;
    }

    /**
     * Build new graphic control extension with given delay & disposal method
     */
    protected function buildGraphicControlExtension(
        GifDataStream $source,
        int $delay,
        DisposalMethod $disposalMethod = DisposalMethod::BACKGROUND
    ): GraphicControlExtension {
        // create extension
        $extension = new GraphicControlExtension($delay, $disposalMethod);

        // set transparency index
        $control = $source->getFirstFrame()->getGraphicControlExtension();
        if ($control && $control->getTransparentColorExistance()) {
            $extension->setTransparentColorExistance();
            $extension->setTransparentColorIndex(
                $control->getTransparentColorIndex()
            );
        }

        return $extension;
    }

    /**
     * Build table based image object from given source
     */
    protected function buildTableBasedImage(
        GifDataStream $source,
        int $left,
        int $top,
        bool $interlaced
    ): TableBasedImage {
        $block = new TableBasedImage();
        $block->setImageDescriptor(new ImageDescriptor());

        // set global color table from source as local color table
        $block->getImageDescriptor()->setLocalColorTableExistance();
        $block->setColorTable($source->getGlobalColorTable());

        $block->getImageDescriptor()->setLocalColorTableSorted(
            $source->getLogicalScreenDescriptor()->getGlobalColorTableSorted()
        );

        $block->getImageDescriptor()->setLocalColorTableSize(
            $source->getLogicalScreenDescriptor()->getGlobalColorTableSize()
        );

        $block->getImageDescriptor()->setSize(
            $source->getLogicalScreenDescriptor()->getWidth(),
            $source->getLogicalScreenDescriptor()->getHeight()
        );

        // set position
        $block->getImageDescriptor()->setPosition($left, $top);

        // set interlaced flag
        $block->getImageDescriptor()->setInterlaced($interlaced);

        // add image data from source
        $block->setImageData($source->getFirstFrame()->getImageData());

        return $block;
    }

    /**
     * Encode the current build
     *
     * @throws EncoderException
     */
    public function encode(): string
    {
        return $this->gif->encode();
    }
}