Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEP Upgrade to intervention/image 3 #621

Merged

Conversation

GuySartorelli
Copy link
Member

@GuySartorelli GuySartorelli commented Jul 9, 2024

Also add strict typing to Image_Backend and InterventionBackend.

Most changes are documented to some extent in the Upgrade Guide.
Changes to exceptions aren't documented, but it basically boils down to:
NotReadableException => DecoderException
NotSupportedException => EncoderException
NotWritableException => EncoderException
ImageException => RuntimeException

Issue

@GuySartorelli GuySartorelli marked this pull request as draft July 9, 2024 23:01
@GuySartorelli GuySartorelli mentioned this pull request Jul 9, 2024
5 tasks
Comment on lines -813 to -816
//skip the `getImageResource` method because we don't want to load the resource just to destroy it
if ($this->image) {
$this->image->destroy();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intervention/image#1273 That method is no longer available and there's no replacement.

@@ -23,7 +23,7 @@
"silverstripe/framework": "^6",
"silverstripe/vendor-plugin": "^2",
"symfony/filesystem": "^6.1",
"intervention/image": "^2.7.2",
"intervention/image": "^3.7",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to go with the latest version in the end because it introduces some API that we need.

Comment on lines -34 to +35
return $this->supportedByIntervention($fromExtension, $backend) && $this->supportedByIntervention($toExtension, $backend);
/** @var InterventionBackend $backend */
$driver = $backend->getImageManager()->driver();
return $driver->supports($fromExtension) && $driver->supports($toExtension);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is built in now! 🎉 No need for our hardcoded nonsense lol

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes in this interface are mostly just adding strict typing to reduce the chance of developer error or things accidentally working and later being regressed.

Any changes not related to strict typing will be explicitly commented on.

Comment on lines -276 to -280
// Fix image orientation
try {
$resource->orientate();
} catch (NotSupportedException $e) {
// noop - we can't orientate, don't worry about it
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orientate() was replaced with orient() - but that happens automatically by default now, so we don't have to do it manually here.

Comment on lines -583 to +488
return $resource->resize(
$width,
$height,
function (Constraint $constraint) use ($useAsMinimum) {
$constraint->aspectRatio();
if (!$useAsMinimum) {
$constraint->upsize();
}
}
);
if ($useAsMinimum) {
return $resource->scale($width, $height);
}
return $resource->scaleDown($width, $height);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $useAsMinimum is true, upscaling is allowed. If false, scaleDown() is used to restrict scaling to only allow reducing the image size.

Comment on lines 150 to 126
public function paddedResize($width, $height, $backgroundColor = "FFFFFF", $transparencyPercent = 0);
public function paddedResize(string $width, string $height, string $backgroundColour = 'FFFFFF', int $transparencyPercent = 0): static;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change color to colour - these are abstracted by the ImageManipulation trait anyway so most people proably won't be calling it directly, much less with named arguments.

Comment on lines 159 to 131
public function croppedResize($width, $height);
public function croppedResize(int $width, int $height, string $position): static;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added $position argument for advanced control, should it be needed.

Comment on lines 169 to 142
public function crop($top, $left, $width, $height);
public function crop(int $top, int $left, int $width, int $height, string $position, string $backgroundColour = 'FFFFFF'): static;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added $position and $backgroundColour arguments for advanced control, should it be needed.

Comment on lines 643 to 538
// caclulate the background colour
$background = $resource->getDriver()->parseColor($backgroundColor)->format('array');
// convert transparancy % to alpha
$background[3] = 1 - round(min(100, max(0, $transparencyPercent)) / 100, 2);
if ($transparencyPercent < 0 || $transparencyPercent > 100) {
throw new InvalidArgumentException('$transparencyPercent must be between 0 and 100. Got ' . $transparencyPercent);
}

$bgColour = Color::create($backgroundColour);
// The Color class is immutable, so we have to instantiate a new one to set the alpha channel.
// No need to do that if both the $backgroundColor and $transparencyPercent are 0.
if ($bgColour->channel(Alpha::class)->value() !== 0 && $transparencyPercent !== 0) {
$channels = $bgColour->channels();
$alpha = (int) round(255 * (1 - ($transparencyPercent * 0.01)));
$bgColour = new Color($channels[0]->value(), $channels[1]->value(), $channels[2]->value(), $alpha);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the upgrade guide:

It is no longer possible to pass color values as array

Passing as a Color object instead. Alpha now has to be between 0 and 255, not 0 and 1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gd used to be used because it was defined as the default in the Intervention\Image\ImageManager class.

Now we have to define one ourselves - so it'll be imagick if the extension is installed, or gd as a fallback.

Comment on lines -69 to +72
} catch (ImageException $e) {
} catch (RuntimeException $e) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is Intervention's special exception, not the base one.... I can add an alias if you want to avoid confusion.

Comment on lines 96 to 99
/**
* Get the current quality (between 0 and 100)
*/
public function getQuality(): int;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is in InterventionBackend and we use it in tests without checking if the object is InterventionBackend or some other class using this interface. Seems sensible to just add it as a required method.

* @var AssetContainer
*/
private $container;
private ?AssetContainer $container = null;
Copy link
Member Author

@GuySartorelli GuySartorelli Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and some of the other properties below) need to have an explicit default value set to avoid the following error:

Typed property must not be accessed before initialization

This error doesn't happen when it's not strictly typed. PHP is weird.

@GuySartorelli GuySartorelli force-pushed the pulls/3/intervention-3 branch 3 times, most recently from 6c42cdb to c786bef Compare July 10, 2024 03:58
Comment on lines -36 to +37
$content = $file->Filename . ' ' . str_repeat('x', 1000000);
$file->setFromString($content, $file->Filename);
$sourcePath = __DIR__ . '/FileLinkTrackingTest/' . $file->Name;
$file->setFromLocalFile($sourcePath, $file->Filename);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to have a real image to avoid the following error:

ImagickException: negative or zero image size `/var/www/html/silverstripe-cache/user/interventionimage_rcPBnB.jpg' @ error/image.c/CloneImage/794

@@ -78,7 +78,7 @@ public function testFileRenameUpdatesDraftAndPublishedPages()

// Live and stage pages both have link to public file
$this->assertStringContainsString(
'<img alt="" src="/assets/FileLinkTrackingTest/testscript-test-file.jpg"',
'<img width="300" height="300" alt="" src="/assets/FileLinkTrackingTest/testscript-test-file.jpg"',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have a real image, it has a height and a width.

@GuySartorelli GuySartorelli force-pushed the pulls/3/intervention-3 branch 5 times, most recently from 49c56fa to 549c349 Compare July 10, 2024 23:09
Comment on lines -97 to +82
private $quality;
private int $quality = AbstractEncoder::DEFAULT_QUALITY;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to set this default value because a: we can't pass a null in anymore, and b: there are unit tests that fail if we set it to arbitrary values like 0 or 100.

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting warning when trying to install both PRs locally:

If this hangs for a long time, then you are probably debugging in phpstorm or vscode
PHP Fatal error: Declaration of SilverStripe\Assets\ImageBackendFactory::create($service, array $params = []) must be compatible with SilverStripe\Core\Injector\Factory::create(string $service, array $params = []): ?object in /var/www/vendor/silverstripe/assets/src/ImageBackendFactory.php on line 38

@GuySartorelli GuySartorelli force-pushed the pulls/3/intervention-3 branch 2 times, most recently from de44096 to fc77d38 Compare July 17, 2024 22:18
@GuySartorelli
Copy link
Member Author

I'm getting warning when trying to install both PRs locally

Rebased both PRs - it was because of the new strict typing on Factory which was done after these PRs were created.

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know where the fish image came from and future people won't either, so just change it some generic silverstripe logo test image e.g. silverstripe/admin/tests/behat/features/files/file1.jpg - it's fine if you just duplicate the image

The performance of large animated gifs under both Imagick and GD seems to be atrocious, things go veeery slow in the file manager on my local after uploading an large animated gif.

I validated that I my configuration to switch between GD and Imagick was correct via debugging get_class($this->getImageManager()->driver()) in silverstripe/assets InterventionBackend.php

I tried uploading https://i.pinimg.com/originals/60/5f/6a/605f6ab7beccce2cb2083f97d013445a.gif and the file manager was unresponsive for quite a while while it uploaded. After it was uploaded the file manager was much slower simply because this animated gif was in the list, even when viewing the files of a different file in the same folder. Archiving the animated gif resolved the performance issues

Given how bad this is, unless this is some sort of bug on our end, I can't help thinking we should actually disable animated gif support, otherwise people are going to inadvertently kill their file manager with a new feature we're promoting.

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Jul 18, 2024

I don't know where the fish image came from and future people won't etierh

It was already being used for another test in this module, I just copied it for use here.
I don't want to replace the existing usage of it because there are different qualities of the image that I don't know how to reproduce.

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Jul 18, 2024

What is the "file manager"? If you mean asset admin, that's why thumbnails don't generate animated GIFs by default (see the asset-admin PR) - so we're providing sane default behaviour already. Given that.... I'm not sure what the problem is?

@emteknetnz
Copy link
Member

Yes asset-admin

Here's a video of what I mean by performance is atrocious

https://www.youtube.com/watch?v=C5iu1w6010o

@GuySartorelli
Copy link
Member Author

Huh. Something about that gif sucks lol, the sample gifs I was using didn't have that problem but I can absolutely reproduce that problem using the gif you used. I'll look into it.

@GuySartorelli GuySartorelli force-pushed the pulls/3/intervention-3 branch 3 times, most recently from bb5b70e to f716212 Compare July 21, 2024 23:50
@GuySartorelli
Copy link
Member Author

GuySartorelli commented Jul 21, 2024

Turns out checking if an image is animated requires fully reading the image, which is slow for animated images.
I've resolved this by making the driver not be a singleton (which matches up with pre-upgrade behaviour anyway) and setting decodeAnimation to false temporarily in the driver when creating a thumbnail.
I've updated the kitchen sink CI run linked in the issue description as well to ensure these changes are okay when both PRs are included.

This will also be reflected in the docs, along with the option to disable it globally via yaml if people want to just avoid it altogether. I reckon leave that up to developers but leave it enabled by default, since in cases where they do want the animated result, it only needs to take the extra time the first time the variant is generated.

GIFs are uncommon enough in the kind of websites that tend to be built in Silverstripe CMS that unintended animated gif manipulations aren't likely to be a major problem, and the times where they are uploaded the content authors probably expect the animation to be retained. So disabling it globally imo should be an intentional decision by project developers.

Comment on lines 144 to 168
/**
* Set whether this image backend is allowed to output animated images as a result of manipulations.
*/
public function setAllowsAnimationInManipulations(bool $allow): static;

/**
* Get whether this image backend is allowed to output animated images as a result of manipulations.
*/
public function getAllowsAnimationInManipulations(): bool;

/**
* Check if the image is animated (e.g. an animated GIF).
*/
public function getIsAnimated(): bool;

/**
* Discards all animation frames of the current image instance except the one at the given position. Turns an animated image into a static one.
*
* @param integer|string $position Which frame to use as the still image.
* If an integer is passed, it represents the exact frame number to use (starting at 0). If that frame doesn't exist, an exception is thrown.
* If a string is passed, it must be in the form of a percentage (e.g. '0%' or '50%'). The frame to use is then determined based
* on this percentage (e.g. if '50%' is passed, a frame halfway through the animation is used).
*/
public function crop($top, $left, $width, $height);
public function removeAnimation(int|string $position): ?static;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there are two different but related concepts here.

On one hand, if animation is not allowed then all variants will result in a still frame regardless of whether the original image was animated or not.

On the other hand, if animation is allowed, developers can create a variant which is a still frame of the original animated image using removeAnimation. This could be useful for getting a specific frame (not just the first frame like when animated variants aren't allowed), or for scenarios where you're generating a series of variants and want some to be animated, and some to be still.

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This something still not right with the animation

It's still veeeery slow to do the initial processing, though I guess this is at least documented

However it's not actually fully processing the animation - for example the large animated gif that I provided as a sample https://i.pinimg.com/originals/60/5f/6a/605f6ab7beccce2cb2083f97d013445a.gif - a couple of variants I created only provide a portion of the animation

  • The variant used in asset admin in the right hand panel on inital upload
  • A variant used on the front end when I go $MyImage.ScaleWidth(300), where MyImage is a has_one on Page

Here's a video of what I mean https://youtu.be/nqMl5vD-sRU

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Jul 23, 2024

I don't know that I agree it's "veeeery" slow, but yes it is slower than a non-animated image. That's because it's still generating an animated preview as you noted

The variant used in asset admin in the right hand panel on inital upload

I can disable that by default along with the thumnail generation if you like.

it's not actually fully processing the animation

That's a bug with intervention/image itself. Can reproduce directly using the following:

composer.json

{
    "name": "gsartorelli/pulls-621",
    "require": {
        "intervention/image": "^3.7"
    }
}

run.php

<?php

$autoload = 'vendor/autoload.php';
if (!file_exists($autoload)) {
    echo 'autoload file is missing - make sure you ran `composer install`.' . PHP_EOL;
    exit(1);
}
include_once $autoload;

use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Imagick\Driver;

// create new manager instance with desired driver
$manager = new ImageManager(new Driver());

// read image from filesystem
$image = $manager->read('605f6ab7beccce2cb2083f97d013445a.gif');

// scale and save result
$image->scale(300)->save('resizedimage.gif');

Other gifs I've tested with don't have this problem, but I'll open a bug report with intervention/image for this specific gif as it may happen with other gifs as well. (see Intervention/image#1380)

@emteknetnz
Copy link
Member

emteknetnz commented Jul 23, 2024

I can disable that by default along with the thumnail generation if you like.

Yeah it should be disabled - the docs PR already says that it should be disabled

Because of this, the ThumbnailGenerator will provide still images as thumbnails for animated gifs by default.

@GuySartorelli
Copy link
Member Author

That's talking about ThumbnailGenerator specifically, which is why it says that :p It doesn't say "for all images anywhere in the CMS".

Also add strict typing to Image_Backend and InterventionBackend.
@GuySartorelli
Copy link
Member Author

GuySartorelli commented Jul 23, 2024

Added a new Image.allow_animated_preview configuration property which devs can use to allow image previews to be animated.
This preview is used in asset admin, and anywhere you can "use existing" files (e.g. UploadField and the WYSIWYG image modal).

I'll update the docs accordingly.

It's worth noting that with exception of that one busted gif, I didn't find any of my test gifs were causing a bad lag when generating that preview (there's a slight one but the result of the animated preview outweighs that short wait IMO)

You might want to try a few other gifs as well with this new config set to true to determine whether it's really warranted. I think it's most likely that the bug with that specific gif is what's causing the slow down.

@emteknetnz
Copy link
Member

I think it's most likely that the bug with that specific gif is what's causing the slow down.

The problem is that any "busted" gif makes for a really bad UX, and end-users (content authors) will think that there's simply something wrong with the CMS. We have no way of knowing how many animated gifs that get uploaded will be of the "busted" variety. We basically need to handle the worst case scenario at the expense of "normal" animated gifs

@emteknetnz emteknetnz merged commit 16444e4 into silverstripe:3 Jul 23, 2024
9 checks passed
@emteknetnz emteknetnz deleted the pulls/3/intervention-3 branch July 23, 2024 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants