Problem/Motivation

Currently, when an Image object is instantiated by the factory, a one-to-one ImageToolkit object is instantiated too and associated to the Image. Most of the methods on the Image object just defer execution to their ImageToolkit counterpart. This works but it's a bit confusing, duplicates methods, and requires the ImageToolkit object to keep a pointer to its Image object, and vice versa.

Some discussions happened in the past with @fietserwin and @claudiu.cristea to 'unify' Image and ImageToolkit, but that was premature at that stage. Now the Image object is much slimmer and we can think about this again.

Further explanation of the idea

Looking outside Drupal (at other Image libraries):

  • If GD would be rewritten in a OOP way, there would be a GdImage class that contains the resource and provides all the operations.
  • Imagick is already 1 class (with a few helper classes for color, text and drawing) containing bot the image and the operations.
  • Imagine offers 1 interface (ImageInterface) and various implementations (GdImage, ImagickImage, GMagickImage). ImageInterface defines "all" the operations like load, save, resize, rotate, etc. Thus also here: data and operations in the same object.

This suggests that sticking with 1 Image interface and an implementation of that per underlying toolkit is the normal way to go.

Looking at sound OO principles:

  • Keep data and operations in 1 class.
  • To much coupling between 2 components is a code smell. The 1-1 relation between Image and ImageToolkit is such a code smell.
  • Proxy classes are a code smell: Image is quite empty, it forwards almost all calls to its strongly coupled toolkit brother.

Looking at what the objects actually represent:

  • Image kind of represents an Image file. You can instantiate it passing in the name of an image file and you can save it. But you can also apply operations to it that change the image.
  • ImageToolkit represents an existing image manipulation library and as such only defines the operations you can apply to an image canvas.

But an object that represent an image file is not needed in the image system. If you want to approach an image as a file, use a file object (issue #2257163: Restrict image system to image processing). Moreover, there is also an issue that want to enable us to start manipulating with a blank canvas, not an image file (#2063373: Cannot save image created from scratch).

But why is it the way it is?
Simply: historical reasons. In D7, the image system was not OOP, there was an image object and toolkit operations were defined based on a naming scheme. OOP'ing this system in D8 was done by simply defining classes around existing concepts, not refactoring or rearchitecting the system.

Proposed resolution

This issue/patch:

  • Removes Drupal\Core\Image\Image altogether. Methods that do not just forward the call to the toolkit will be merged into ImageToolkitBase.
  • Keeps Drupal\Core\Image\ImageInterface and Drupal\Core\ImageToolkit\ImageToolkitInterface, but deduplicates methods that appear in both.
  • Makes toolkits implement both interfaces.
  • Changes the image factory to deliver ImageToolkit plugin objects in place of the Image objects.

By limiting this patch to the above, the changes for the rest of the system are minimal. This keeps the patch readable. The patch in the follow up will be much larger, but will mainly consist of moving around and renaming.

Remaining tasks

  • This issue: agree and commit.
  • Follow up #2336811: Remove Image, we only need ImageToolkit:
    • merge ImageInterface and ImageToolkitInterface into Imageinterface?this will mostly be a lot of moving around with renaming
    • merge ImageFactory and ImageToolkitManager: by merging these 2 classes/interfaces, Image becomes a plugin and thus we should have a manager, no longer a factory.
    • are the now final methods on ImageToolkitBase and their do... counterparts (pareFile -> doParseFile and save -> doSave) logical or can some further refactoring/integration be done there?
    • restore the "value" behavior of Image objects. An image object should not be reused for different files or for creating different blank canvases. So there should be no methods to set the file or create a blank canvas, this should be part of the constructor.
  • Follow up #2109459: Review image test suite: are the tests sets still logical. Currently, we have quite some duplication in the test, This patch will probably already reduce this, but further clean up remains needed.

User interface changes

None.

API changes

Removes a few methods:
ImageInterface:

  • ImageInterface::getToolkit()
  • ImageInterface::getToolkitId()

ImageToolkitInterface ("internal", normally not used outside the image system, in fact this is mostly deduplication):

  • ImageToolkitInterface::setImage()
  • ImageToolkitInterface::getImage()
  • ImageToolkitInterface::isValid() (already in ImageInterface)
  • ImageToolkitInterface::save() (already in ImageInterface)
  • ImageToolkitInterface::parseFile() (moved to ImageInterface)
  • ImageToolkitInterface::getHeight() (already in ImageInterface)
  • ImageToolkitInterface::getWidth() (already in ImageInterface)
  • ImageToolkitInterface::getMimeType() (already in ImageInterface)
  • ImageToolkitInterface::apply() (already in ImageInterface)

Comments

mondrake’s picture

Status: Active » Needs review
StatusFileSize
new21.31 KB

Just to see the bot response... PHPUnit tests not covered.

mondrake’s picture

Status: Needs review » Needs work

The last submitted patch, 1: 2331481-no_image-1.patch, failed testing.

mondrake’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new24.03 KB
new45.34 KB

The Image class is removed, so there's no purpose to keep a PHPUnit test for it... changed the existing test to GDToolkitTest which is what it is actually doing, and moved from core test directory to a directory of the system module, which is where the GD toolkit (currently) belongs.

mondrake’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 4: 2331481-no_image-4.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new470 bytes
new45.35 KB

Oh yeah

mondrake’s picture

Issue tags: +beta target

Green. Adding 'beta target' tag to stimulate discussion before beta...

mondrake’s picture

Status: Needs review » Needs work

The last submitted patch, 9: 2331481-no_image-9.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new1.95 KB
new46.77 KB

Rerolled + fixed tests.

fietserwin’s picture

Issue summary: View changes

(As expected) I fully agree with this idea. I updated the summary with the original reasoning behind this idea and further follow-up work.

I will try to review shortly.

fietserwin’s picture

Issue summary: View changes
Status: Needs review » Needs work
  1. diff --git a/core/lib/Drupal/Core/Image/ImageInterface.php b/core/lib/Drupal/Core/Image/ImageInterface.php
    

    use Drupal\Core\Image\ImageInterface;
    is no longer used

  2. +++ b/core/lib/Drupal/Core/Image/ImageInterface.php
    @@ -13,6 +13,20 @@
       /**
    +   * Determines if a file contains a valid image.
    +   *
    +   * Drupal supports GIF, JPG and PNG file formats when used with the GD
    +   * toolkit, and may support others, depending on which toolkits are
    +   * installed.
    +   *
    +   * @param @todo
    +   *
    +   * @return bool
    +   *   TRUE if the file could be found and is an image, FALSE otherwise.
    +   */
    +  public function parseFile($source);
    +
    

    Does this have to be part of the interface? I cannot find any real usage, only test and upon creation? If it is needed for testing purposes, we should find another way as to not clutter the interface. (If it should be part of the interface there's a @todo.)

    In the merging we lost the source parameter on the constructor. I propose to reintroduce that. In doing so we can remove this method. Note that we currently cannot change the source after an Image object has been created and, IMO, that should remain so.

  3. +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitBase.php
    @@ -79,23 +85,118 @@ public function __construct(array $configuration, $plugin_id, array $plugin_defi
    +    if ($this->doParseFile()) {
    +      $this->fileSize = filesize($this->source);
    

    Add doParsefile() as an abstract method to document what toolkit implementers should do. Same for doSave() (see e.g. ImageToolkitOperationBase).

I'm happy that a first merge can be done with a relative small sized patch. I have identified some follow-ups but those will not really change the idea behind this patch and as such are not necessary to do directly in this patch.

mondrake’s picture

@fietserwin thanks

1. sorry I do not understand, can you elaborate
2. reintroducing the $source parameter in the constructor of a plugin class would mean to reintroduce a tailor made createInstance() method on ImageToolkitManager, which we just got rid of in #2096703: Image toolkits should use PluginFormInterface and ContainerFactoryPluginInterface with the benefit of allowing injection of any service in a toolkit. So I'll leave it in the interface and let the factory make the call to parseFile() (similarly, we may have the factory make a call to a createNew() method in #2063373: Cannot save image created from scratch on getting non-file based images). I'll fix the todo and reintroduce the checking that parseFile() cannot be called twice.
3. Sure

fietserwin’s picture

ad 1) there's a now no longer used use statement at the top of that file.

ad 2) Then name it setSource() or createFromSource() as that mathces better with createNew())

mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new7.74 KB
new49.2 KB

All done. Let's see if bot is happy.

tim.plunkett’s picture

This class used to be a value object. Whatever broken state it is in now was caused by the refactoring done since it was added.

fietserwin’s picture

re #16:
i did a new review and, given the follow-ups, I find this patch acceptable and a very good step into the right direction, therefore: RTBC, and I will create the follow-ups shortly.

re #17:
I am not sure what class you are talking about when you refer to "this class":
- if it is Image: that indeed used to be some kind of value object (not fully though), but that class will be gone with this patch.
- If you mean ImageToolkit (i.e. an object implementing ImageToolkitInterface): IMO this has never been a value object, it was and is the "in memory representation of an image (~ canvas) including a hint to its origin/destination". As such it has never been a value object, as we should be able to apply image operations to it that change the image canvas, as well as that we can save to another destination than its origin (image derivative), thereby changing the file info as well (that is currently also already done that way in Image).

If you are not happy with the createFromSource() method: neither am I, but due to @mondrake's reasoning in #14.2, I think that is better addressed in the follow up that will merge the image factory and image toolkit manager.

fietserwin’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update, +Needs change record

We need the issue summary to be updated with what the final patch is doing. And it'd be nice to have one CR that all the Image / ImageToolkit architecture issues link to.

tim.plunkett’s picture

I meant the class you are removing. Drupal\Core\Image\Image. It is a value object that represents an image, and now it has been consumed by ImageToolkit, which now represents two things.
And now the follow-up merges two more distinct classes.

By the time this is all done, we will have monolithic multipurpose classes, with no separation of concerns.

fietserwin’s picture

RE #21: Thanks for clarifying, but I can not fully agree with you.

IMO, Image is a mixture of 1) redundant file functionality and 2) a proxy to image toolkit functionality.

ad 1)
Image contains getFileSize, getMimeType and getSource(). To me this is functionality better handled by a file class. Looking at current usage, there is no place where an Image object is used as both a file and as an image canvas. To get these methods out of Image, I filed (a while ago) issue #2257163: Restrict image system to image processing.

ad 2) All other methods on Image are forwarded to the toolkit object. Most without any further processing. Some with some default processing that is assumed to be the same for all toolkits or is needed to keep its own state correct. The former can be handled by a base class implementation of that method or, as this patch does, via a template pattern. The latter disappears on merging.

The way this patch (and other issues) is heading to, is the way that the Imagine library handles different "toolkits". You have an Image interface defining methods to open, alter and save an image (file) and you have actual implementations in the form of a Gd\Imagine class or an Imagick\Imagine class, that alter the image using the given toolkit. This is simple and plain OO, data and operations in 1 object, common functionality in a base class, and in this case I don't see a need to make it more complicated than that.

fietserwin’s picture

Title: Remove Drupal\Core\Image\Image » Merge Image and ImageToolkitBase
Issue summary: View changes
Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs issue summary update, -Needs change record

I updated the title and issue summary. I will update the issue summary of the follow up as well.

The "overall" change record is https://www.drupal.org/node/2084547. As this change record is already published, we prefer to update it directly after committing issues.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

In talking to @catch about this issue I ended up repeating some of the statements of the issue summary - if you want to treat an image as a file use the file object and I was wondering what Imagine does. So I went to look at Imagine and it does not really do this. If you look at documentation https://imagine.readthedocs.org/en/latest/ it is up to the app that uses image to decide what toolkit to use. The way in which Image and ImageToolkit are coupled using the plugin manager is how we do this in Drupal 8. So Image is a proxy - it is a proxy onto the chosen toolkit - I think this is okay.

mondrake’s picture

#24 - if we do not want to do this, fair enough. The bigger picture would be in #2336811: Remove Image, we only need ImageToolkit. There we are in fact proposing to convert the current Image to be a plugin, and remove the entire concept of 'toolkit'. We do not need a middleman between an Image object and the image library/module, in our opinion: an ImageBase class would be extended by plugin classes and contain all the code and data needed to access image libraries/modules like GD, Imagemagick, etc.

If I understand well, this would also address @tim.plunkett concerns about the Image class having been eroded since its introduction, with many properties having been moved from Image to ImageToolkit -- there the specific image plugin class will have its own properties back.

alexpott’s picture

Status: Needs review » Closed (duplicate)

But the image is not a plugin - the toolkit is - the image is composed of both file and an instance of the toolkit plugin. But yep if we are going to do this now we need to do whatever we do in one step - halfway steps aren't practical now since they move us further away from release - so I'm closing this issue to continue discussions on #2336811: Remove Image, we only need ImageToolkit