Add option to not upscale images that are too small to fit target scale

Created on 16 April 2019, over 5 years ago
Updated 5 September 2024, 3 months ago

It would be really nice if the scale & crop image effect had an option to not upscale images that are too small for the target scale, but to still crop the image to the largest size that the original image will accommodate.

This has been brought up twice before for Drupal 7:

But I'd like to submit this idea for re-consideration in D8. Some of my image styles (especially responsive image styles) are quite large, but the source image may not be large enough for all of them. Focal point upscales the image to fit the image style, which is a waste of resources and reduces quality.

Basically with no upscaling is selected, a check is performed to see if the source image is too small for the target dimensions. If so, the target dimensions are shrunk to allow the complete original image to fix within it. As a result, the image that's output will be smaller than the desired target size, but the aspect ratio of the original target is still preserved.

The code needed to make this work is quite simple. The image effect code can be modified as such:


  /**
   * {@inheritdoc}
   */
  public function applyEffect(ImageInterface $image) {
    parent::applyEffect($image);

    if ($this->configuration['no_upscale']) {
      // If there's not enough image to completely fill the target dimensions,
      // then shrink the target dimensions so that it does fit. Without doing so,
      // focal point will upscale the image to fit within the target dimensions,
      // and we want to avoid that.
      $target_width = $this->configuration['width'];
      $target_height = $this->configuration['height'];
  
      // There's three cases to handle. The first if the source image is
      // smaller than the target dimensions in both width and height.
      if ($target_width >= $image->getWidth() && $target_height >= $image->getHeight()) {
        $target_ratio = $target_width / $target_height;
        $source_ratio = $image->getWidth() / $image->getHeight();
        // Depending on how the A/Rs compare between the source and target,
        // figure out if we need to adjust the target's width or height.
        if ($target_ratio > $source_ratio) {
          $this->adjustTargetToMatchSourceWidth($image);
        }
        else {
          $this->adjustTargetToMatchSourceHeight($image);
        }
      }
      // The second case is if just the image width is smaller than the target
      // width.
      elseif ($target_width >= $image->getWidth()) {
        $this->adjustTargetToMatchSourceWidth($image);
      }
      // And the final case is if just the image height is smaller than the
      // target height.
      elseif ($target_height >= $image->getHeight()) {
        $this->adjustTargetToMatchSourceHeight($image);
      }
    }

    $originalDimensions = $this->getOriginalImageSize();
    $resize_data = self::calculateResizeData($originalDimensions['width'], $originalDimensions['height'], $this->configuration['width'], $this->configuration['height']);
    if (!$image->resize($resize_data['width'], $resize_data['height'])) {
      $this->logger->error(
        'Focal point scale and crop failed while resizing using the %toolkit toolkit on %path (%mimetype, %dimensions)',
        [
          '%toolkit' => $image->getToolkitId(),
          '%path' => $image->getSource(),
          '%mimetype' => $image->getMimeType(),
          '%dimensions' => $image->getWidth() . 'x' . $image->getHeight(),
        ]
      );
      return FALSE;
    }

    $crop = $this->getCrop($image);
    return $this->applyCrop($image, $crop);
  }

  private function adjustTargetToMatchSourceHeight(ImageInterface $image) {
    $target_ratio = $this->configuration['width'] / $this->configuration['height'];
    $this->configuration['height'] = $image->getHeight();
    $this->configuration['width'] = (int) ($this->configuration['height'] * $target_ratio);
  }

  private function adjustTargetToMatchSourceWidth(ImageInterface $image) {
    $target_ratio = $this->configuration['width'] / $this->configuration['height'];
    $this->configuration['width'] = $image->getWidth();
    $this->configuration['height'] = (int) ($this->configuration['width'] / $target_ratio);
  }

Is this something that will be considered? For now I have this code in my own image style, but this seems like it would be great to have in the module for everyone else as well.

Feature request
Status

Needs review

Version

2.0

Component

Other Code

Created by

🇺🇸United States bkosborne New Jersey, USA

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.5 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    158 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.5 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update over 1 year ago
    158 pass
  • 🇺🇸United States DamienMcKenna NH, USA
  • 🇺🇸United States j_ten_man

    Changing all of the >= checks to >. This was causing issues for me where if the image is already scaled to the correct width or height, then it was still recalculating a new value. The comments in the patch seem to indicate that it should actually be just > and not >=.

  • Status changed to Needs work over 1 year ago
  • 🇺🇸United States j_ten_man

    This patch is experiencing the issue that was pointed out in #17. I'll work on a fix for it, but the patch on 21 and 23 don't work because we need to implement the transformDimensions method and correctly handle when we're not scaling the image.

  • Status changed to Needs review over 1 year ago
  • 🇺🇸United States j_ten_man

    And here is a lightly tested patch. Did some code re-organization to re-use the same logic for calculating the dimensions when the img tag is generated and when the image is actually resized.

  • 🇺🇸United States recrit

    @j_ten_man your patch #25 is missing the focal_point_post_update_upscale_scale_crop from patch #21. Please re-add this update and post an interdiff.txt comparing your changes to patch #21.

  • 🇺🇸United States j_ten_man

    Here's the corrected patch with the interdiff.

  • Open in Jenkins → Open on Drupal.org →
    Core: 10.0.7 + Environment: PHP 8.1 & MariaDB 10.3.22
    last update about 1 year ago
    158 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.2 & MySQL 8
    last update about 1 year ago
    158 pass
  • 🇺🇸United States recrit

    @j_ten_man thanks for the updated patch. Altering the dimensions in transformDimensions is closer to how core does tit in \Drupal\image\Plugin\ImageEffect\ScaleImageEffect::transformDimensions().

    The patch is missing the empty width and height check similar to \Drupal\image\Plugin\ImageEffect\ScaleImageEffect::transformDimensions(). This prevents division by zero errors when calculating the aspect ratios. Example: The blazy module could call with the NULL width and height if it cannot get the dimensions, example and SVG.

    The attached patch adds the empty check for the dimensions.

  • Status changed to Needs work about 1 year ago
  • 🇺🇸United States DamienMcKenna NH, USA

    I'm seeing strange behavior with #28 when applied to 8.x-1.x.

    My image style only has one effect: Focal Point Scale and Crop, width of 1200, height of 600, and upscale is set to "Don't upscale but still crop".

    However, the image style preview page shows 800x400, and the label says "Focal Point Scale and Crop 800×400 - Upscaling: Don't upscale but still crop".

    The second thing is that I'm not sure it's working correctly.

    I have an image that is 1600x300. When it's passed through this image style it comes out as 600x300. Shouldn't it be 1200x300?

  • Status changed to Needs review about 1 year ago
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.2 & MySQL 8
    last update about 1 year ago
    158 pass
  • 🇺🇸United States recrit

    The attached patch fixes a major issue with the previous patches. The previous patches were altering the "width" and "height" in the $this->configuration in order to pass it to the parent ResizeImageEffect::transformDimensions(). However, altering the instance configuration will corrupt the entity cache for the image style since it caches the effects plugin instances in ImageStyle::effectsCollection. This can lead to unexpected results when multiple images are on the page using the same image style. The image style effect instance's configuration width and height will always be set to the last image's transformed dimensions.

    @DamienMcKenna please re-test as this change may fix the odd behaviour that you were seeing.

  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.2 & MySQL 8
    last update about 1 year ago
    158 pass
  • 🇺🇸United States recrit

    Added the missing downsize case to the transformDimensions method.

  • Status changed to Needs work about 1 year ago
  • 🇧🇪Belgium matthiasm11

    Patch #31 doesn't give the desired result.

    The image style preview page has an image of 800x600. When applying "Focal Point Scale and Crop 1400×1000 - Upscaling: Don't upscale but still crop", the image is still upscaled to 1400x1000, while I would expect it to return the original image (800x600).

  • Status changed to Needs review about 1 year ago
  • Open in Jenkins → Open on Drupal.org →
    Core: 10.1.x + Environment: PHP 8.2 & MySQL 8
    last update about 1 year ago
    158 pass
  • 🇺🇸United States recrit

    @matthiasm11 I have updated the patch. Please re-test. There were a few other areas where the adjusted crop dimensions needed to be used (see the interdiff for a comparison).

  • 🇧🇪Belgium matthiasm11

    This one works, thanks!

  • Open in Jenkins → Open on Drupal.org →
    Core: 10.2.x + Environment: PHP 8.2 & MySQL 8
    last update 10 months ago
    158 pass
  • 🇩🇪Germany yannickoo Berlin

    Really nice to see that this has been fixed, thank you so much to everyone who was working on this 💪

    I did some minor tweaks like

    • Using × instead of × unicode directly in Twig template
    • Change upsale config select list label from If image is too small: to Upscaling behavior
    • Fix indentation
  • #35 also works, thank you!

  • Open in Jenkins → Open on Drupal.org →
    Core: 10.2.x + Environment: PHP 8.2 & MySQL 8
    last update 7 months ago
    159 pass
  • 🇮🇳India rajeshreeputra Pune

    @yannickoo can we get MR for the same.

  • 🇨🇦Canada druplr

    Thanks everyone!!

    Having the "Upscale and crop" and "Don't upscale but still crop" options is very useful. However, I am wondering what is the expected behavior of "Don't upscale and don't crop" (no_upscale_no_crop) option regarding cropping? From its label, it seems that it should not crop the image. Right? Or maybe I'm missing something?

    But with focal_point 2.1.0 and focal_point-3048398-37.patch and no_upscale_no_crop option selected, it still crops the image:

    Please see the config below:

    uuid: 1fe0eeae-1be1-42ab-8d47-c8b8160ad6c1
    langcode: en
    status: true
    dependencies:
      module:
        - focal_point
    name: test
    label: test
    effects:
      ba78a9d7-fcd3-4acd-a764-bc0e3b4a3fc1:
        uuid: ba78a9d7-fcd3-4acd-a764-bc0e3b4a3fc1
        id: focal_point_scale_and_crop
        weight: 1
        data:
          width: 20
          height: 100
          crop_type: focal_point
          upscale: no_upscale_no_crop
    

    Also, the "don't crop" part seems contradictory with focal_point module's mission:

    Focal Point allows you to specify the portion of an image that is most important. This information can be used when the image is cropped or cropped and scaled so that you don't, for example, end up with an image that cuts off the subject's head.

    If we do not intend to crop, can't we just use core's image_scale instead? It has an option to upscale or not.

  • 🇺🇸United States recrit

    MR 38 created from the patch #37 Add option to not upscale images that are too small to fit target scale Needs review . The patch 37 can be used for any composer builds and applies to the latest 2.x.

  • Pipeline finished with Failed
    3 months ago
    Total: 145s
    #275175
Production build 0.71.5 2024