5
votes

This is functional code to reduce an image down to a specified smaller size. But it has several things that are not good:

  • it's slow
  • it can do several iterations before getting the scaled image
  • each time it has to determine the size it has to load the entire image into a memoryStream

I would like to improve it. Can there be some way to get a better initial estimate to preclude so many iterations? Am I going about this all wrong? My reasons for creating it is to accept any image of unknown size and scale it to a certain size. This will allow better planning for storage needs. When you scale to a certain height/width, the image size can vary far too much for our needs.

You will need a ref to System.Drawing.

    //Scale down the image till it fits the given file size.
    public static Image ScaleDownToKb(Image img, long targetKilobytes, long quality)
    {
        //DateTime start = DateTime.Now;
        //DateTime end;

        float h, w;
        float halfFactor = 100; // halves itself each iteration
        float testPerc = 100;
        var direction = -1;
        long lastSize = 0;
        var iteration = 0;
        var origH = img.Height;
        var origW = img.Width;

        // if already below target, just return the image
        var size = GetImageFileSizeBytes(img, 250000, quality);
        if (size < targetKilobytes * 1024)
        {
            //end = DateTime.Now;
            //Console.WriteLine("================ DONE.  ITERATIONS: " + iteration + " " + end.Subtract(start));
            return img;
        }

        while (true)
        {
            iteration++;

            halfFactor /= 2;
            testPerc += halfFactor * direction;

            h = origH * testPerc / 100;
            w = origW * testPerc / 100;

            var test = ScaleImage(img, (int)w, (int)h);
            size = GetImageFileSizeBytes(test, 50000, quality);

            var byteTarg = targetKilobytes * 1024;
            //Console.WriteLine(iteration + ": " + halfFactor + "% (" + testPerc + ") " + size + " " + byteTarg);

            if ((Math.Abs(byteTarg - size) / (double)byteTarg) < .1  ||  size == lastSize  ||  iteration > 15 /* safety measure */)
            {
                //end = DateTime.Now;
                //Console.WriteLine("================ DONE.  ITERATIONS: " + iteration + " " + end.Subtract(start));
                return test;
            }

            if (size > targetKilobytes * 1024)
            {
                direction = -1;
            }
            else
            {
                direction = 1;
            }

            lastSize = size;
        }
    }

    public static long GetImageFileSizeBytes(Image image, int estimatedSize, long quality)
    {
        long jpegByteSize;
        using (var ms = new MemoryStream(estimatedSize))
        {
            SaveJpeg(image, ms, quality);
            jpegByteSize = ms.Length;
        }
        return jpegByteSize;
    }

    public static void SaveJpeg(Image image, MemoryStream ms, long quality)
    {
        ((Bitmap)image).Save(ms, FindEncoder(ImageFormat.Jpeg), GetEncoderParams(quality));
    }

    public static void SaveJpeg(Image image, string filename, long quality)
    {
        ((Bitmap)image).Save(filename, FindEncoder(ImageFormat.Jpeg), GetEncoderParams(quality));
    }

    public static ImageCodecInfo FindEncoder(ImageFormat format)
    {

        if (format == null)
            throw new ArgumentNullException("format");

        foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
        {
            if (codec.FormatID.Equals(format.Guid))
            {
                return codec;
            }
        }

        return null;
    }

    public static EncoderParameters GetEncoderParams(long quality)
    {
        System.Drawing.Imaging.Encoder encoder = System.Drawing.Imaging.Encoder.Quality;
        //Encoder encoder = new Encoder(ImageFormat.Jpeg.Guid);
        EncoderParameters eparams = new EncoderParameters(1);
        EncoderParameter eparam = new EncoderParameter(encoder, quality);
        eparams.Param[0] = eparam;
        return eparams;
    }

    //Scale an image to a given width and height.
    public static Image ScaleImage(Image img, int outW, int outH)
    {
        Bitmap outImg = new Bitmap(outW, outH, img.PixelFormat);
        outImg.SetResolution(img.HorizontalResolution, img.VerticalResolution);
        Graphics graphics = Graphics.FromImage(outImg);
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.DrawImage(img, new Rectangle(0, 0, outW, outH), new Rectangle(0, 0, img.Width, img.Height), GraphicsUnit.Pixel);
        graphics.Dispose();

        return outImg;
    }

Calling this will create a 2nd image that is close in size to the requested value:

        var image = Image.FromFile(@"C:\Temp\test.jpg");
        var scaled = ScaleDownToKb(image, 250, 80);
        SaveJpeg(scaled, @"C:\Temp\test_REDUCED.jpg", 80);

For this specific example:

  • original file size: 628 kB
  • requested file size: 250 kB
  • scaled file size: 238 kB
4

4 Answers

1
votes

I think you can assume linear growth (and reducing) of file size depending on pixels count growth. Meaning, if, for example, you've got 500x500 200 kb image and you need 50 kb image, you should reduce image dimensions to 250x250 (4 times less pixels). I belive this should get you a desired image with one iteration most of the time. But you can tweak this even further, by introducing some risk percent (like 10%) to reducing ratio or something like that.

0
votes

Instead of doing a slow set of iterations for each image, do a test with a number representative images and get a resolution that will give you the desired file size on average. Then use that resolution all the time.

0
votes

@jbobbins: I agree with @xpda, if the first attempt to resize the image to the target size is too far away from the threshold, you can repeat the step one more time or simply fall back to your previous inneficient algorithm. It will converge a lot faster than your current implementation. The whole thing should be executed in O(1) instead of O(log n), as you are doing now.

You could sample some JPEG compression ratios and sort of construct a table from experimentation (I know it won't be perfect, but close enough) that will give you a very good approximation. For example (taken from Wikipedia):

Compression Ratio            Quality
     2.6:1                    100
      15:1                     50
      23:1                     25
      46:1                     10
0
votes

My solution to this problem was to reduce the quality until the desired size was reached. Below is my solution for posterity.

NB: This could be improved by doing some kind of guesswork.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

namespace PhotoShrinker
{
    class Program
    {
    /// <summary>
    /// Max photo size in bytes
    /// </summary>
    const long MAX_PHOTO_SIZE = 409600;

    static void Main(string[] args)
    {
        var photos = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.jpg");

        foreach (var photo in photos)
        {
            var photoName = Path.GetFileNameWithoutExtension(photo);

            var fi = new FileInfo(photo);
            Console.WriteLine("Photo: " + photo);
            Console.WriteLine(fi.Length);

            if (fi.Length > MAX_PHOTO_SIZE)
            {
                using (var stream = DownscaleImage(Image.FromFile(photo)))
                {
                    using (var file = File.Create(photoName + "-smaller.jpg"))
                    {
                        stream.CopyTo(file);
                    }
                }
                Console.WriteLine("Done.");
            }
            Console.ReadLine();
        }

    }

    private static MemoryStream DownscaleImage(Image photo)
    {
        MemoryStream resizedPhotoStream = new MemoryStream();

        long resizedSize = 0;
        var quality = 93;
        //long lastSizeDifference = 0;
        do
        {
            resizedPhotoStream.SetLength(0);

            EncoderParameters eps = new EncoderParameters(1);
            eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);
            ImageCodecInfo ici = GetEncoderInfo("image/jpeg");

            photo.Save(resizedPhotoStream, ici, eps);
            resizedSize = resizedPhotoStream.Length;

            //long sizeDifference = resizedSize - MAX_PHOTO_SIZE;
            //Console.WriteLine(resizedSize + "(" + sizeDifference + " " + (lastSizeDifference - sizeDifference) + ")");
            //lastSizeDifference = sizeDifference;
            quality--;

        } while (resizedSize > MAX_PHOTO_SIZE);

        resizedPhotoStream.Seek(0, SeekOrigin.Begin);

        return resizedPhotoStream;
    }

    private static ImageCodecInfo GetEncoderInfo(String mimeType)
    {
        int j;
        ImageCodecInfo[] encoders;
        encoders = ImageCodecInfo.GetImageEncoders();
        for (j = 0; j < encoders.Length; ++j)
        {
            if (encoders[j].MimeType == mimeType)
                return encoders[j];
        }
        return null;
    }
}
}