High quality, high performance thumbnails in Flash

Ever need a thumbnail of an image in Flash? I do, and honestly speaking, the resampling that Flash does is less than ideal. Unless you only need to resize by half or bigger. But my thumbnails usually need to be smaller.

I searched for a solution and found on voq.com a promising library with some algorithms that worked quite nicely and a demo. The quality was nice, but the speed was slow. It also fiddled with the color a little bit. If I put the "easyScaling" parameter down from .5 to .25 I ended up with a nicer thumbnail but was slower to make and had more color disfiguration. You could tell what the original piece was better though.

After some more searching I found that Brooks Andrus played with some algorithms using Pixel Bender. The conclusion to that was he found he could do the same thing with the "smoothing" option in the BitmapData.draw() method. And this still leaves me with yucky thumbnails at small sizes.

I began thinking I would need to take the algorithms from the first source and port them into pixel bender like Brooks did for his ThumbGenie application.

I thought I'd try one more option first. I had played around in my mind with the idea of resizing a bitmap in half, then in half again, until I reached my destination size, since resizing by half still had good results. After discussing it with Tyler I tried it out and ended up with some great results. That last resize to get to the final thumbnail size wasn't half because in an imperfect world your thumbnails aren't always a power of two times smaller than your original. So I had an odd-man-out scale at the end I was applying. Tyler suggested I put that odd-man-out scale at the first instead. So instead of:

1000x750 * .5 resize * .5 resize * .5 resize * .8 = 100x75

It worked like this:

1000x750 * .8 resize * .5 resize * .5 resize * .5 = 100x75

Doing it that way landed me with even better results! It seems that Flash does it's best work when resizing by exactly 1/2.

Thumbnail tests

The above image shows my test results as follows taking a snapshot of my homepage in an HTMLLoader in AIR.

  1. Regular BitmapData.draw() method without smoothing (0 miliseconds)
  2. Regular BitmapData.draw() method with smoothing (1 miliseconds)
  3. Using my own method that resizes by the odd scale first, then by halves (8 milliseconds)
  4. Using my method but after the odd scale, only scaling down by quarters (33 miliseconds)
  5. Using the voq Lanczos3 method with easyScaling at .25 and sharpening (694 miliseconds)
  6. Using the voq Lanczos3 method with easyScaling at .25 and no sharping (676 miliseconds)
  7. Using the voq Triangle method with easyScaling at .25 and sharpening (275 miliseconds)
  8. Using the voq Triangle method with easyScaling at .25 and no sharpening (266 miliseconds)

My personal opinion is that the best looking thumb is #3. I was surprised that #4 wouldn't be as good. I knew the performance would be lower because I was doing more iterations of BitmapData.draw(), but I thought that scaling it to 75% (down a quarter) each time would end up nicer. Looks like 50% is the best scale to use.

The voq.com algos looked pretty decent, but obviously were slow. I'm quite happy with the solution I've found. I only had to write a little bit of code, it's fast enough, and looks better than any other solution I've found (even Fireworks resizing IMO).

Here is the method I wrote for this. Note, when scaling up, it seemed to look better to just use smoothing and do it in one draw() and not iterations of 2.

private static const IDEAL_RESIZE_PERCENT:Number = .5;

public static function resizeImage(source:BitmapData, width:uint, height:uint, constrainProportions:Boolean = true):BitmapData
{
    var scaleX:Number = width/source.width;
    var scaleY:Number = height/source.height;
    if (constrainProportions) {
        if (scaleX> scaleY) scaleX = scaleY;
        else scaleY = scaleX;
    }
   
    var bitmapData:BitmapData = source;
   
    if (scaleX>= 1 && scaleY>= 1) {
        bitmapData = new BitmapData(Math.ceil(source.width*scaleX), Math.ceil(source.height*scaleY), true, 0);
        bitmapData.draw(source, new Matrix(scaleX, 0, 0, scaleY), null, null, null, true);
        return bitmapData;
    }
   
    // scale it by the IDEAL for best quality
    var nextScaleX:Number = scaleX;
    var nextScaleY:Number = scaleY;
    while (nextScaleX <1) nextScaleX /= IDEAL_RESIZE_PERCENT;
    while (nextScaleY <1) nextScaleY /= IDEAL_RESIZE_PERCENT;
   
    if (scaleX <IDEAL_RESIZE_PERCENT) nextScaleX *= IDEAL_RESIZE_PERCENT;
    if (scaleY <IDEAL_RESIZE_PERCENT) nextScaleY *= IDEAL_RESIZE_PERCENT;
   
    var temp:BitmapData = new BitmapData(bitmapData.width*nextScaleX, bitmapData.height*nextScaleY, true, 0);
    temp.draw(bitmapData, new Matrix(nextScaleX, 0, 0, nextScaleY), null, null, null, true);
    bitmapData = temp;
   
    nextScaleX *= IDEAL_RESIZE_PERCENT;
    nextScaleY *= IDEAL_RESIZE_PERCENT;
   
    while (nextScaleX>= scaleX || nextScaleY>= scaleY) {
        var actualScaleX:Number = nextScaleX>= scaleX ? IDEAL_RESIZE_PERCENT : 1;
        var actualScaleY:Number = nextScaleY>= scaleY ? IDEAL_RESIZE_PERCENT : 1;
        temp = new BitmapData(bitmapData.width*actualScaleX, bitmapData.height*actualScaleY, true, 0);
        temp.draw(bitmapData, new Matrix(actualScaleX, 0, 0, actualScaleY), null, null, null, true);
        bitmapData.dispose();
        nextScaleX *= IDEAL_RESIZE_PERCENT;
        nextScaleY *= IDEAL_RESIZE_PERCENT;
        bitmapData = temp;
    }
   
    return bitmapData;
}

Enjoy!

Update: I was getting unsatisfactory images when the resize scale was more than 50%. For example, the original code posted would size an image from 100x100 to 60x60 using one BitmapData.draw() step. And it didn't look that great.

I found that if I sized the image up to a scale that allowed it to be size back down by exactly 50% that the results were much better. In the above example, the 100x100 would scale up by 120% to 120x120, then scale down 50% to 60x60. The final image looks much better this way. The code has been updated to work like this. It also had the option to turn constrain proportions off.

Update: I've posted my code library to Google Code. You can see my final implementation of bitmap resizing in the ImageUtils class.



20 Responses to “High quality, high performance thumbnails in Flash”

  1. Tyler Wright says:

    That’s awesome Jac! It’s almost like you have this really cool twin brother who helped you a little too.

    ;) No shame in crediting myself.

    Seriously, I’m glad to see this. It ended up looking very nice.

    It might be a good idea to optimize and use the same (large) bitmapData to do the scale reduction (just draw over previous pixels) and then dump() it after doing a final copy over to the new thumbnail. Just a suggestion

  2. Alan Wayman says:

    Jac,

    This is awesome, I have been looking into this very thing for a Photo Gallery I have been working on. Until now I have loaded both the Images and the Thumbnails but this will make it a lot easier to update and maintain. Thanks,

    Alan

  3. Bjorn says:

    Does setting the stage quality from good to best make much difference in the times?

  4. Jacob Wright says:

    Tyler: I tried reusing the bitmap, but unexpected and random articles would make it into the final image when doing that. Good idea though.

    Bjorn: The stage quality I believe only affects rendering to screen and not the pixel data of the images. I wouldn’t think changing the screen quality makes any difference.

  5. Max Scheer says:

    Very neat thingy you wrote. Thanks for sharing. I think one thing should be added. Bitmap-Function just works with your own images (same server). Caching images from third party isn’t working.
    If well, please tell me how :) I ran into that problem once and couldn’t solve it.

  6. Jacob Wright says:

    Max: Try loading your images with URLLoader first, then take the byteArray you get from that and load it into Loader using Loader.loadBytes(byteArray);

    While this isn’t very secure for your application, if you know you are accessing safe content this should work out. The security issue comes because instead of an image, you may be loading a SWF with code that does bad things. But if you own both servers then you should be fine.

  7. Dr. Green says:

    Thanks to You for sharing this awesome idea. I tryed to get more perfect result, and if you use some sharpening _before_ the last resizing (i did not know yet, that it will required every pass, or enough before the last), then it will be more clear result, but not too sharp, like sample 6.

  8. kindmonkey says:

    Wow! good idea!! Thanks!! :D

  9. Curtis says:

    I had be struggling to get decent thumbs without sacrificing huge amounts of time. You just saved me a bunch of work. Thank You.

  10. [...] I’m using my ImageUtils class which ensures it looks good when resized (more about that in another post). Then I turn the BitmapData object into a PNG with AS3CoreLib’s PNGEncoder. I now have the [...]

  11. Deniss says:

    Hello Jacob,

    First of all please excuse my bad English and thank you for sharing this amazing idea and code :)

    I have used your code to build a thumbnail gallery and everything works and looks perfect. Unfortunately I met one problem when decided to scale down (80% for example) these thumbs. I mean to scale down a movieclips that contain resized ( with resizeImage() ) JPGs. I did this because I wanted to make a simple RollOver effect for each thumb. On RollOver I have tween of the given movieclip _xscale=_yscale=100 and on RollOut I have tween _xscale=_yscale=80 … (just for the record I am using TweenMax of greensock.com )

    So when I scale down these movieclips I have the problem with antiliasing although all used .draw() methods have parameter smooth=true … unfortunately movieclip’s parameter forceSmoothing=true is working only with externally loaded (with loadMovie()) images but not with images created with BitmapData and attachBitmap …

    I am very curious if there is some solution of that problem and will be very thankful to hear some opinions/ideas

    Thanks in advance and once again big congrats for you work :)

  12. Jacob Wright says:

    Deniss, when you are using the code I provided above and BitmapData.draw() you have complete control over how you want it resized. (e.g. You can do several iterations to bring it smaller or whatever, like this post shows).

    However, when using Flash’s scaling, 3D, rotation, etc. Flash has control over how it gets drawn. You have no control over it. So when you adjust the scale of the MovieClip Flash is doing the drawing.

    If you want the best quality, you’ll need to redraw this manually. A simple option would be to have the thumb start out at 80% of the size and the _xscale/_yscale at 100. Then on rollover replace the thumb with the 100% size and set the scale to 80, and let it tween to 100. When you roll out, tween the scale back to 80 and once it is done, replace the thumb with an 80% sized thumb and set the scale to 100.

    This way you are only using the default Flash rendering while it is tweening, but you use the bitmap rendering when it is at rest.

    I hope that makes sense. Good luck!

  13. Deniss says:

    Wow this is so simple and sounds so logical … I can’t believe how fast you answer me and even gave me so good idea …

    Thanks I wish you everything best :)

  14. Scott R. says:

    Jacob,

    I stumbled upon your class today and found it quite a nice little utility. I ran into one major issue that I wanted to pass on, in terms of memory leaks with giant .jpg files.

    Within ImageUtils.as if you add after the following lines the following code, your memory usage will go from 100’s of Megabytes to 100’s of Kilobytes:

    After Line 146: source.dispose();

    After Line 160: bitmapData.dispose();

    Even though you are reassigning your temp value to bitmapData and source, the bytes allocated for the initial giant images are still kept in memory.

    Doing dispose() before a reassignment ensures that all byte data is destroyed.

    I can send you an updated file if you like.

    Best,

    -Scott

  15. Jacob Wright says:

    Thanks Scott! I’ve updated it in the repo. Good catch. I’m ashamed I didn’t think about that before. BitmapData 101. Or maybe 201.

  16. judah says:

    Hi Jacob,

    Great class! :)

    I was trying to see the images but they are too small. Can you upload a large version so we can do comparisons? THX

  17. Jacob Wright says:

    They are small because the were resized using the class. I didn’t make them smaller to fit in my blog post. The original size was 1000×750. I was using a HTMLLoader in AIR, loaded up my website at the time, and made thumbnails of the page using the different mechanisms down to 100×75.

    So my class in the end resizes this particular size from 1000×750 to 800×600 first (the odd size first and halves after that), then 400×300, then 200×150, then 100×75. This was the best result of all the methods.

  18. Corey says:

    Keep in mind that if the global stage.quality is set to BEST, the quality of your down scaled thumbnails equates to what the player will give you in this context.

    Your approach is still of value of course, because a stage.quality of BEST has *many* performance implications, certainly valuable to be able to opt-in to HQ as needed.

  19. Cleverson says:

    Hi Jacob, i’m trying to use it on Flash CS4 but I’m having some errors: 1120: Access of undefined property ResizeStyle.

  20. Jacob Wright says:

    The utils class has a dependency on the ResizeStyle for constants. It’s all in the image package: http://code.google.com/p/jacwright/source/browse/trunk/flash/jac/src/jac/image/

    Be sure to include ImageUtils and ResizeStyle in your classpath.

Leave a Reply