follow-up of #2211227: Refactor image and imagetoolkit: isExisting, isSupported, supportedTypes, getMimeType

Problem/Motivation

While working on #2211227: Refactor image and imagetoolkit: isExisting, isSupported, supportedTypes, getMimeType, we realized that in the GD toolkit we are currently *always* loading fully an image into a GD resource. This is unnecessary overhead, as the actual GD resource is needed only if an image needs to be manipulated by applying operations to it. In most cases, $imageFactory->get() will only be needed to determine MIME type, height and width to deliver an image to the client, which can be achieved by only calling getimagesize(), and avoiding the current calls to image_type_to_extension(), imagecreatefromxxx(), and even overlaying if an image is not truecolor.

Proposed resolution

Split the logic:

  • ImageToolkitInterface::parseFile() only run getimagesize(), and store its results in a 'preload' property that will be used by getMimeType, getHeight, getWidth if the GD resource has not been created yet
  • fully load the image in the GD resource only when an operation has to be applied on it, and getMimeType, getHeight, getWidth will then return their values dynamically from the resource

In doing so, move the image validity checking fully into the toolkit.

Remaining tasks

  • review patch

User interface changes

Nope

API changes

One method added to ImageToolkitInterface and implemented in GD and Test toolkits: isValid(). ImageInterface::isValid() just defers execution to the toolkit,

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mondrake’s picture

Assigned: Unassigned » mondrake
Issue summary: View changes
Status: Postponed » Active

Blockers are in, back to active

mondrake’s picture

Status: Active » Needs review
FileSize
3.3 KB

Test only. We expect that when we get a fresh image from the factory, the load() method is not invoked, not even after calling getImage(), getWidth(), etc.

Status: Needs review » Needs work

The last submitted patch, 2: 2244359-1-test-only.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review
FileSize
9 KB

Here's a patch. This introduces a new setValid() method on ImageInterface. This is because parseFile() and load() now are called in different times and may lead to independent errors so invalidating the image. This method will also be useful later when in #2063373: Cannot save image created from scratch we will need to validate a blank image after some processing (when we get an image via $image = \Drupal::service('image.factory')->get(); we are currently getting an image that has the $valid flag set to FALSE).

Status: Needs review » Needs work

The last submitted patch, 4: 2244359-4.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review
FileSize
2.32 KB
10.19 KB

The test failure in #4 is not related to load() method being called, but on the toolkit id returned by testGetToolkitId() being null instead of 'gd'. This is due to the way the stubs array was built in the test. This patch fixes the test in that respect + code standard and docs.

mondrake’s picture

Issue summary: View changes
mondrake’s picture

Assigned: mondrake » Unassigned
fietserwin’s picture

Status: Needs review » Needs work
Related issues: +#2257163: Restrict image system to image processing
  1. +++ b/core/lib/Drupal/Core/Image/ImageInterface.php
    @@ -21,6 +21,16 @@
    +   * Sets the image validity flag.
    +   *
    +   * @param bool $valid
    +   *   TRUE if the image object contains a valid image, FALSE otherwise.
    +   *
    +   * @return $this
    +   */
    +  public function setValid($valid);
    +
    

    Please no, not another method on the interface that means nothing to the outside world. If Image needs info from the toolkit about the validity of the image source, add a method on toolkit that gets called by image.

  2. +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
    @@ -50,11 +57,18 @@ public function setResource($resource) {
    +    if (isset($this->resource)) {
    +      return $this->resource;
    +    }
    +    elseif ($this->getImage()->isValid() && isset($this->preLoadInfo) && $this->load()) {
    +      unset($this->preLoadInfo);
    +      return $this->resource;
    +    }
    +    return NULL;
       }
    

    This seems like too much activity and logic in a getter: this should end up as something like:

    $this->load();
    return $this->resource;
    

    Where load has a quick return path or like:

    if (!$this->resource) {
      $this->load();
    }
    return $this->resource;
    

    When load() will always execute the loading.

  3. +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
    @@ -215,6 +238,9 @@ public function createTmp($type, $width, $height) {
    +    if (isset($this->preLoadInfo)) {
    +      return $this->preLoadInfo[0];
    +    }
         return $this->getResource() ? imagesx($this->getResource()) : NULL;
       }
    

    In cases where we have 3 options, I would prefer using the same logic, thus either a nested ternary or an if/elseif/else, not an if followed by a ternary. (Same for getHeight)

  4. +++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php
    @@ -92,19 +92,31 @@ protected function getToolkitOperationMock($class_name, ImageToolkitInterface $t
    +   * @param bool $load_expected
    +   *   (optional) Whether the load() method is expected to ba called. Defaults
    +   *   to TRUE.
        * @param array $stubs
    

    be (instead of ba)

The first point will completely change the patch, so a new extensive review will be needed by then. However, I also doubt if this whole exercise is needed at all, as I prefer to reduce the functionality of the image system further according to the (now) related issue #2211227: Refactor image and imagetoolkit: isExisting, isSupported, supportedTypes, getMimeType. If we do that, there won't remain (m)any cases where an image object is created that will not undergo any processing. (Image field dimensions validation and then deciding that it is not needed to scale down is probably such a case.)

What do you think?

mondrake’s picture

Status: Needs work » Needs review
FileSize
7.12 KB
11.41 KB

Thanks @fietserwin, all done. In fact point 1 is very valid, I moved the entire validity check at the toolkit level. Re. more generic point on #2257163: Restrict image system to image processing, I would only say that, no matter what, we need to be able to manage the image validity if we want to address #2063373: Cannot save image created from scratch: currently if we get() an image from the factory with no source file specified, it will be invalid, and there are no ways to make it valid through an operation.

Status: Needs review » Needs work

The last submitted patch, 10: 2244359-10.patch, failed testing.

Status: Needs work » Needs review

mondrake queued 10: 2244359-10.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 10: 2244359-10.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review
FileSize
2.48 KB
11.59 KB

Fix for the failing test + cleanup.

fietserwin’s picture

Status: Needs review » Needs work

Great, this is much better. I would RTBC it in this state, but would even more appreciate it if the few nitpicks below are solved. These are not all are introduced by this patch here, but as this patch does touch the code, we might as well improve it here in this patch. So you may set it to NR (with a new patch) or RTBC if you think that these should not be part of this patch.

  1. +++ b/core/lib/Drupal/Core/Image/Image.php
    @@ -157,10 +150,10 @@ public function save($destination = NULL) {
        * @return bool
        *   FALSE, if the file could not be found or is not an image. Otherwise, the
        *   image information is populated.
        */
       protected function parseFile() {
    -    if ($this->valid = $this->getToolkit()->parseFile()) {
    

    This is not properly documenting the return value but the desired side effect of the function.

  2. +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
    @@ -26,7 +26,7 @@ class GDToolkit extends ImageToolkitBase {
        *
        * @var resource
        */
    -  protected $resource;
    +  protected $resource = NULL;
     
    

    @var resource|null (as we accept and explicitly handle null values for this property).

  3. +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
    @@ -36,6 +36,13 @@ class GDToolkit extends ImageToolkitBase {
       /**
    +   * Image information from a file, available prior to loading the GD resource.
    +   *
    +   * @var array
    +   */
    +  protected $preLoadInfo = NULL;
    +
    

    - @ var array|null (as we accept and explicitly handle null values for this property).
    - We may consider more explicitly documenting what info from a file it contains, i.e. refer to @see imagegetsize().

  4. +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
    @@ -44,6 +51,9 @@ class GDToolkit extends ImageToolkitBase {
    +    if ($this->preLoadInfo) {
    +      $this->preLoadInfo = NULL;
    +    }
         $this->resource = $resource;
    

    The if is not necessary.

mondrake’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
3.36 KB
13.14 KB

Thanks.

All done.

Re. #15.1 - I took a step further and just removed parseFile() from Image, I believe we no longer need a separate method here.

Adjusted issue summary.

fietserwin’s picture

Status: Needs review » Reviewed & tested by the community

Top, even better so.

  • webchick committed fd0be04 on 8.0.x
    Issue #2244359 by mondrake: Lazy-load GD resource in GDToolkit.
    
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Hm. That $load_expected param kinda hurts my brain, but I guess as long as it's isolated to tests it's not a huge deal.

Committed and pushed to 8.x. Thanks!

fietserwin’s picture

Thanks, added a remark to issue #2109459: Review image test suite about that param.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.