php / php-src

The PHP Interpreter
https://www.php.net
Other
37.97k stars 7.73k forks source link

Support cloning of GDImages #15636

Open KarelWintersky opened 2 weeks ago

KarelWintersky commented 2 weeks ago

Description

I want do something like this:

$f0 = (new Image('test.jpg'))->load()->setQuality(90);

$f1 = $f0->applyFilter(IMG_FILTER_GRAYSCALE)
    ->setFilename('test_grayscale_1.jpg')
    ->save();

$f2 = $f0->applyFilter(IMG_FILTER_NEGATE)
    ->setFilename('test_inverted.jpg')
    ->save();

$f3 = $f0->applyFilter(IMG_FILTER_GRAYSCALE)
    ->setFilename('test_grayscale_2.jpg')
    ->save();

now, test_grayscale2.jpg is inverted AND grayscale, not an only grayscale, as expected.

Is any way to clone GDImage resource without black magic like imgcopy, imgcrop, etc?

cmb69 commented 2 weeks ago

Well, there is #10241 and #14487 which both would use gdImageClone(), but expose it in a different way.

KarelWintersky commented 2 weeks ago

PR 3 years ago...

is there any hope that this will be implemented into the kernel at least in php 8.4?

maybe it is possible to use this method through the FFI ?

KarelWintersky commented 2 weeks ago

but expose it in a different way.

even if it requires using this method in the following way - it will still be a great solution:

$f0 = (new ImageWrapper('test.jpg'))->load()->setQuality(90);

$f1 = $f0
     ->clone()
     ->applyFilter(IMG_FILTER_GRAYSCALE)
    ->setFilename('test_grayscale_1.jpg')
    ->save();

$f2 = $f0
     ->clone()
    ->applyFilter(IMG_FILTER_NEGATE)
    ->setFilename('test_inverted.jpg')
    ->save();

$f3 = $f0
  ->clone()
  ->crop(/* args */)
  ->rotate(/* args */)
  ->setQuality(70)
  ->setMimeType('image/webp')
  ->setFilename('test.webp')
  ->save();

Of course, here ImageWrapper is a wrapper over the built-in functions for working with images, I do not provide the class definition due to its obvious.

The class definition code is obvious and elementary.

KarelWintersky commented 2 weeks ago

my current example (!) code that simulates cloning a GdIResource:

class IMG {
    private string $filename;
    private string $extension;
    public $data;
    public int $quality = 90;
    public $actions = [];

    public function __construct($filename)
    {
        $this->filename = $filename;
        $this->extension = pathinfo($this->filename, PATHINFO_EXTENSION);
    }

    public function load():self
    {
        $im = imagecreatefromjpeg($this->filename);
        $this->data = $im;
        return clone $this;
    }

    public function save($quality = null):static
    {
        $this->quality = is_null($quality) ? $this->quality : $quality;
        $this->quality = is_null($this->quality) ? 90 : $this->quality;
        $this->valid = imagejpeg($this->data, $this->filename, $this->quality);
        return clone $this;
    }

    public function clone()
    {
        $target = (new static($this->filename))->load();

        // apply actions

        foreach ($this->actions as $action) {
            $method = $action[0];
            $params = $action[1] ?? [];

            if (!is_callable([$target, $method])) {
                throw new RuntimeException("Unknown or uncallable method `{$method}`");
            }

            $target = call_user_func_array([$target, $method], $params);
        }
        return $target;
    }

    // actions

    public function setQuality(int $quality):static
    {
        $this->quality = $quality;
        $this->actions[] = [ __METHOD__, [ $quality ]];
        return clone $this;
    }

    public function setFilename(string $filename):static
    {
        $this->filename = $filename;
        $this->actions[] = [ __METHOD__, [ $filename ]];
        return clone $this;
    }

    public function applyFilter(int $filter, ...$args):static
    {
        $this->actions[] = [ __METHOD__, [ $filter, [ $args ]]];
        imagefilter($this->data, $filter, $args);
        $this->filters[] = $filter;
        return clone $this;
    }
// etc
}

$f0 = (new IMG('test.jpg'))->load()->setQuality(100);

$f0->clone()
    ->applyFilter(IMG_FILTER_NEGATE)
    ->setFilename('test_inverted.jpg')
    ->save();

$f0->clone()->applyFilter(IMG_FILTER_GRAYSCALE)
    ->setFilename('test_grayscale_1.jpg')
    ->save();

$f0->clone()->applyFilter(IMG_FILTER_GRAYSCALE)
    ->setFilename('test_grayscale_2.jpg')
    ->save();

It works, the result matches the expected (although I haven't tested it on all the image processing methods I need yet), but it's not a real cloning of the resource, but a recreation of it based on the existing file.

Why cloning an image resource wasn't possible to implement at the language level before is a complete mystery to me. Maybe there is a reason, but it's incomprehensible to me.

cmb69 commented 2 weeks ago

Why cloning an image resource wasn't possible to implement at the language level before is a complete mystery to me. Maybe there is a reason, but it's incomprehensible to me.

I don't think there is any particular reason, besides that it probably a functionality which is not required that often.

KarelWintersky commented 2 weeks ago

Generating multiple preview images from a single image is a typical task for photo galleries...

cmb69 commented 2 weeks ago

Okay. What would you prefer: imageclone($im) or clone $im?

KarelWintersky commented 2 weeks ago

A complex and difficult question.

The first call is a function call, and the second appeals to the general mechanism of cloning objects in PHP.

I think it would be correct to implement the magic method __clone() , which should (in theory) be called as a result of the syntactic construction

$gdImageInstance_2 = clone $gdImageInstance_1;

And this method should be implemented via imageclone() (along with the necessary checks and exception throws, according to good secure programming practices)

That is, it would be correct to implement both methods.

But I am not sure that you should rely on a single private opinion and it would be worthwhile to put this issue to a vote of the development team (at least) or real users (ideally).

(sorry for google translate)