Multi resolution images for different display scales (retina etc)


#1

Is there any way to define multiple image scales for a single image in Juce? Would it be a good idea to make a new multi resolution image group type class for this purpose?

Being able to group multiple resolutions of the same image together in some way and then using this group like a regular image is useful to abstract target display properties away from the logic of drawing images.

Here is a little helper class to get the ball rolling, it's not very efficiently coded but at least works for a non affine transformed component. I'm using this to blit the background image to my scalable user interface

struct ImageWithScale
{
    ImageWithScale ()
    : image ()
    , scale (1.0f)
    {
    }
    ImageWithScale (Image im, float sc)
    : image (im)
    , scale (sc)
    {
    }
    ImageWithScale (const ImageWithScale& other)
    : image (other.image)
    , scale (other.scale)
    {
    }
    ImageWithScale& operator= (const ImageWithScale& other)
    {
        if (this != &other)
        {
            image = other.image;
            scale = other.scale;
        }
        return *this;
    }
    Image image;
    float scale;
};

struct ImageWithScale
{
    ImageWithScale ()
    : image ()
    , scale (1.0f)
    {
    }
    ImageWithScale (Image im, float sc)
    : image (im)
    , scale (sc)
    {
    }
    ImageWithScale (const ImageWithScale& other)
    : image (other.image)
    , scale (other.scale)
    {
    }
    ImageWithScale& operator= (const ImageWithScale& other)
    {
        if (this != &other)
        {
            image = other.image;
            scale = other.scale;
        }
        return *this;
    }
    Image image;
    float scale;
};

class ImageMultiRes
{
public:
    ImageMultiRes ()
        : width (0.0f)
        , height (0.0f)
        {};
    ImageMultiRes (float w, float h)
        : width (w)
        , height (h)
        {};
    void setSize (float w, float h)
    {
        jassert (images.size () == 0);
        width = w;
        height = h;
    }
    void addImage (Image im)
    {
        jassert (width > 0.0f);
        jassert (height > 0.0f);
        float scalex = im.getWidth () / width;
        float scaley = im.getHeight () / height;
        // all the images need to have the same aspect ratio
        jassert (fabsf (scalex - scaley) < 1e-6f);
        images.add (ImageWithScale (im, scalex));
    }
    void drawImage (Graphics& g)
    {
        drawImageScaled (g, 1.0f);
    }
    void drawImageScaled (Graphics& g, float scale)
    {
        #if defined (MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7)
        juce::Point<int> p = getScreenPosition ();
        float screenscale = Desktop::getInstance().getDisplays().getDisplayContaining (p).scale;
        #else
        float screenscale = g.getInternalContext().getTargetScale ();
        #endif
        float whichscale = scale*screenscale;
        ImageWithScale* whichimage;
        float closest = 1e10f;
        for (int i=0; i<images.size (); i++)
        {
            ImageWithScale& im = images.getReference (i);
            float distance = whichscale/im.scale;
            if (distance < closest)
            {
                closest = distance;
                whichimage = &im;
                if (closest == 1.0f)
                {
                    // prefer a direct 1:1 blit
                    break;
                }
            }
        }
        float targetscale = scale / whichimage->scale;
        g.drawImageTransformed (whichimage->image, AffineTransform::scale (targetscale));
    }
    float width, height;
    Array <ImageWithScale> images;
};

 

I'm targetting OS 10.5, so I had to hack in this to the low level graphics context to be able to detect the screen scale:


float CoreGraphicsContext::getTargetScale ()
{
    const CGAffineTransform t = CGContextGetUserSpaceToDeviceSpaceTransform (context);
    return (float) (juce_hypot (t.a, t.c) + juce_hypot (t.b, t.d)) / 2.0f;
}

 

You can use it like this:

ImageMultiRes multires (width, height);
multires.addImage (im0);
multires.addImage (im1);
multires.addImage (im2);

The in your paint:

multires.drawImageScaled (graphics, scale);

 

 


#2

Hmm, seems like overkill to me! Are you actually using different images for each resolution, or are you trying to improve rendering performance by avoiding rescaling?


#3

BTW, actually it would be cool, if the SoftwareRenderer has a better resampling quality (<50%) so if you could use just one big image (...or just use  vector graphics, but this is not always possible)


#4

Yes, these are different images. No matter what quality I set the image resampling to in Juce I can't get a better result than using my 3D package to do the anti-aliasing, since it has access to the original vector information and can use up to 1024 samples per pixel in its anti-aliasing fir kernel, so I'll manually set the image to get best results.

For most people they could use this sort of a feature by settings a single high resoluation image, and then use a "very high quality" more sinc like resampler to generate the mip map images which then get blitted to the screen. Juce does not have a "very high quality" resampler at preset, I'm talking about something along the lines of the high quality resamplers in the Anti-Grain cpp package. This would be a 1 time higher cost resampling so that down the line lower cost resampling can be used and still get decent results. The current downsampling of the Juce image resampling is fast, but aliases a lot.


#5

True, I could add a new resampler for the software engine, but not for the CoreGraphics or GL ones, so what I've done in my own apps to get a good-but-slow resize is a quick-and-dirty trick like in ProjectExporter::rescaleImageForIcon(), where it just rescales multiple times by less than a factor of 2 until it reaches the target size.


#6

only the Software-Renderer needs a little bit imrovement, the CoreGraphics Renders works good! Together this are 99% of all use-cases.