Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resize jpeg image to specified size

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
like image 574
jbobbins Avatar asked Nov 13 '22 17:11

jbobbins


1 Answers

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.

like image 164
Kostassoid Avatar answered Nov 24 '22 00:11

Kostassoid