Page tree
Skip to end of metadata
Go to start of metadata

Point of Discussion

Magnolia does not always render images in sufficient quality. This problem occurs sometimes, when magnolia has to scale images to new sizes.

Current Situation

The STK, in conjuction with Magnolia's imaging module, supports image variations configured via the themes. By default, the variation is implemented by the class info.magnolia.module.templatingkit.imaging.generation.SimpleResizeVariation, configured via component configuration in the STK module.

The standard variation class (as the name implies) has a standard image operation chain which loads the image and resizes it using either the info.magnolia.imaging.operations.cropresize.BoundedResize or info.magnolia.imaging.operations.cropresize.AutoCropAndResize image operation classes, depending on whether crop=true is set in the variation.

The BoundedResize or AutoCropAndResize classes in turn make use of a info.magnolia.imaging.operations.cropresize.Resizer class which does the actual resizing. The STK fixes this resizer class to use the implementation info.magnolia.imaging.operations.cropresize.resizers.MultiStepResizer.

Explanation of the Problem

Basically, scaling images is not as simple as it seems. When scaling an image to a new size, usually smaller than the original, each pixel in new image is a combination of several pixels in the original image. How to sample and combine these pixels to make a new pixel is a hard problem to solve, and turns out to be a kind of filtering of the original image.

Done right, the sampling produces a smaller version of the original image that looks, to our eyes, exactly like the original. Done wrong the sampling can introduce artifacts, drop details and otherwise change the resulting output image so that we visually see differences between the images that make us say "this is a bad copy", or "something went wrong shrinking this image".

Shrinking images costs performance, and isn't that easy to do right. For this reason the JDK has provided methods for developers with which they can resize images.

Traditionally this was done using Image.getScaledInstance(), which works very well, produces great quality with its "area averaging" algorithm, but is very slow.

In JDK 1.2 Sun added the Graphics2D API with drawImage() methods to draw scaled versions of images. Due to the poor performance of getScaledInstance() Sun recommended that developers use these new APIs. Unfortunately, although drawImage() can work with several different algorithms, none of them come close to the quality achieved with getScaledInstance().

A proposed solution to this quality problem is what is currently in use in magnolia: the MultiStepResizer attempts to achieve better quality by using the drawImage() method repeatedly to produce scaled images via a series of intermediate scaled resolutions. While this is a big improvement in quality for many cases, it is not a perfect solution:

  • firstly, the multi-step resizing is slow due to the multiple intermediate scaled resolutions it has to compute before arriving at its final answer
  • secondly, it does not cover all cases - for example any image less than twice the size of the desired thumbnail will be scaled in only one step, resulting in the same quality as a single application of drawImage()

More information on all this can be found at:

https://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html

http://www.thebuzzmedia.com/software/imgscalr-java-image-scaling-library/

Proposed Solution

Solution to all this: Use the cool imgscalr library to get good quality and good performance, for all images.

Some snippets to show how:

The following is a Resizer implementation which uses the imgscalr library:

package mypackage.imaging;
 
import java.awt.image.BufferedImage;
import org.imgscalr.Scalr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import info.magnolia.imaging.operations.cropresize.Coords;
import info.magnolia.imaging.operations.cropresize.Resizer;
import info.magnolia.imaging.operations.cropresize.Size;

public class ScalrResizer implements Resizer {
	public final static Logger log = LoggerFactory.getLogger(ScalrResizer.class);
	
	@Override
	public BufferedImage resize(BufferedImage src, Coords srcCoords, Size targetSize) {
		BufferedImage subImg = src;
		if ( srcCoords.getX1()!=0 || srcCoords.getY1()!=0 || srcCoords.getHeight()!=src.getHeight() || srcCoords.getWidth()!=src.getWidth() )
			subImg = src.getSubimage(srcCoords.getX1(), srcCoords.getY1(), srcCoords.getWidth(), srcCoords.getHeight());
		log.debug("Resizing image via Scalr");
		return Scalr.resize(subImg, targetSize.getWidth(), targetSize.getHeight(), null);
	}
}

The following is a replacement variation class which allows you to configure the resizer to be used, either by setting a default resizer via component configuration, or by configuring it in the variation definition:

package mypackage.imaging;
 
import info.magnolia.cms.core.NodeData;
import info.magnolia.imaging.ParameterProvider;
import info.magnolia.imaging.operations.ImageOperationChain;
import info.magnolia.imaging.operations.cropresize.AutoCropAndResize;
import info.magnolia.imaging.operations.cropresize.BoundedResize;
import info.magnolia.imaging.operations.cropresize.Resizer;
import info.magnolia.imaging.operations.load.FromNodeData;
import info.magnolia.module.templatingkit.imaging.generation.ImageOperationProvidingVariation;
import info.magnolia.module.templatingkit.sites.Site;
import info.magnolia.objectfactory.Components;
import javax.inject.Inject;
import javax.inject.Provider;

/**
 * 
 */
public class SimpleResizeVariation extends ImageOperationProvidingVariation {
    private Integer width;
    private Integer height;
    private boolean crop = true;
    
    protected Resizer resizer = null;
    
    @Inject
    public SimpleResizeVariation(Provider<Site> siteProvider) {
        super(siteProvider);
    }
    public void init() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        if (imageOperation == null) {
            final ImageOperationChain<ParameterProvider<NodeData>> chain = new ImageOperationChain<ParameterProvider<NodeData>>();
            chain.addOperation(new FromNodeData());
            if (crop) {
                final AutoCropAndResize resize = new AutoCropAndResize();
                if (resizer==null)
                	resize.setResizer(Components.getComponent(Resizer.class));
                else
                	resize.setResizer(resizer);
                if (getWidth() != null) {
                    resize.setTargetWidth(getWidth());
                }
                if (getHeight() != null) {
                    resize.setTargetHeight(getHeight());
                }
                chain.addOperation(resize);
            }
            else {
                final BoundedResize resize = new BoundedResize();
                if (resizer==null)
                	resize.setResizer(Components.getComponent(Resizer.class));
                else
                	resize.setResizer(resizer);
                if (getWidth() != null) {
                    resize.setMaxWidth(getWidth());
                }
                if (getHeight() != null) {
                    resize.setMaxHeight(getHeight());
                }
                chain.addOperation(resize);
            }
            setImageOperation(chain);
        }
    }
    public Integer getWidth() {
        return this.width;
    }
    public void setWidth(Integer width) {
        this.width = width;
    }
    public Integer getHeight() {
        return this.height;
    }
    public void setHeight(Integer height) {
        this.height = height;
    }
    public boolean isCrop() {
        return this.crop;
    }
    public void setCrop(boolean crop) {
        this.crop = crop;
    }
	public Resizer getResizer() {
		return resizer;
	}
	public void setResizer(Resizer resizer) {
		this.resizer = resizer;
	}
}

The component configuration in your module XML could look like this:

	<!-- define a resizer component and default implementation -->
    <component>
      <type>info.magnolia.imaging.operations.cropresize.Resizer</type>
      <implementation>mypackage.imaging.ScalrResizer</implementation>
    </component>
    
	<!-- override the STK module's variation implementation. Note: make sure this module loads after 'standard-templating-kit' module! -->
    <type-mapping>
      <type>info.magnolia.module.templatingkit.imaging.Variation</type>
      <implementation>mypackage.imaging.SimpleResizeVariation</implementation>
    </type-mapping>

Quality comparison

To motivate all this, here a quick comparison of image quality. In this comparison the same source image was scaled to the same size with a variety of different methods.

Source image:

Scaled results:

drawImage()
bilinear
drawImage()
bicubic
drawImage()
nearest neighbor
multistepgetScaledImage()imgscalr library

Note how the first three images are pretty bad quality. Multistep works ok too in this case.

Image is (c) Richard Unger 2014, all rights reserved worldwide.

 

  • No labels

3 Comments

  1. Maybe it's confluence messing up with the images as well, but I don't see the imgscalr version necessarily nicer than multistep (did you try ScaleAreaAveragingResizer btw?). BUT I'm all for options, so I'd vote for this anyway.

    Imaging module's info.magnolia.imaging.operations.cropresize.AbstractCropAndResize already lets one swap the Resizer implementation, but that might not be taken advantage of in the STK integration. We should probably work on that too.

    1. No, you are totally correct - I chose a bad example because the multistep actually does fine on that image.

      Also, I'm not necessarily suggesting to change the MultistepResizer... I created this page for 2 reasons:

      1) as a point to collect ideas and discussion on this topic

      2) to encourage magnolia to expose the resizer in the STK variation class, so we can (more easily) configure the resizing algorithm when using STK variations. 

      (for point 2, take a look at my version of the SimpleResizeVariation class - I copied yours and exposed the resizer (with a default implementation via component overridable by node2bean)

       

      What I tend to find is that the getScaledInstance (ScaledAreaAveragingResizer) yields the best quality, consistently, but it really is slow.

      The imgscalr resizer also gives consistent good quality, and is faster (at least 2x) than getScaledInstance, but not that fast really.

      The multi-step resizer seems to give good results almost always. I'll try to find a copyright-free sample image for where it doesn't do as well. Its speed depends on the size of the source image in relation to the destination size, but is good in practice.

      The default Graphics2D resizers, regardless of the mode, just have awful quality, one can't use them.