简体   繁体   中英

Resize width on upload and keep the height ratio proportional

I'm using the following system drawing code to resize images on Upload. The problem is that landscape or portrait images get distorted cause the sytem drawing is making them square. Is it possible to resize the width only and keep the height proportional? and How? Thanks

HttpPostedFile imageFile = UploadImages.PostedFile;
                            System.Drawing.Image ri = System.Drawing.Image.FromStream(imageFile.InputStream);
                            ri = ResizeBitmap((Bitmap) ri, 200, 200); 

private Bitmap ResizeBitmap(Bitmap b, int nWidth, int nHeight)
    {
        Bitmap result = new Bitmap(nWidth, nHeight);
        using (Graphics g = Graphics.FromImage((System.Drawing.Image)result))
            g.DrawImage(b, 0, 0, nWidth, nHeight);
        return result;
    }

If what you want to do is create a new bitmap 200 pixels wide and with height scaled proportionately, you can do this:

    private static int CalculateProportionalHeight(int oldWidth, int oldHeight, int newWidth)
    {
        if (oldWidth <= 0 || oldHeight <= 0 || newWidth <= 0)
            // For safety.
            return oldHeight;
        double widthFactor = (double)newWidth / (double)oldWidth;
        int newHeight = (int)Math.Round(widthFactor * (double)oldHeight);
        if (newHeight < 1)
            newHeight = 1; // just in case.
        return newHeight;
    }

    private static Bitmap ResizeBitmap(Bitmap b, int nWidth)
    {
        int nHeight = CalculateProportionalHeight(b.Width, b.Height, nWidth);
        Bitmap result = new Bitmap(nWidth, nHeight);
        using (Graphics g = Graphics.FromImage((System.Drawing.Image)result))
            g.DrawImage(b, 0, 0, nWidth, nHeight);
        return result;
    }

Or are you looking to create a 200x200 bitmap with the old image scaled to fit inside, and letterboxed if necessary?

Update

If you are looking to create an image of a fixed 200x200 size, with the image scaled down proportionately to fit and letterboxed, this should do it:

    static RectangleF PlaceInside(int oldWidth, int oldHeight, int newWidth, int newHeight)
    {
        if (oldWidth <= 0 || oldHeight <= 0 || newWidth <= 0 || newHeight <= 0)
            return new RectangleF(oldWidth, oldHeight, newWidth, newHeight);
        float widthFactor = (float)newWidth / (float)oldWidth;
        float heightFactor = (float)newHeight / (float)oldHeight;
        if (widthFactor < heightFactor)
        {
            // prefer width
            float scaledHeight = widthFactor * oldHeight;
            // new new RectangleF(x, y, width, height)
            return new RectangleF(0, (newHeight - scaledHeight) / 2.0f, newWidth, scaledHeight);
        }
        else
        {
            // prefer height
            float scaledWidth = heightFactor * oldWidth;
            // new new RectangleF(x, y, width, height)
            return new RectangleF((newWidth - scaledWidth) / 2.0f, 0, scaledWidth, newHeight);
        }
    }

    private static Bitmap ResizeBitmap(Bitmap b, int nWidth, int nHeight)
    {
        int oldWidth = b.Width;
        int oldHeight = b.Height;
        Bitmap result = new Bitmap(nWidth, nHeight);
        using (Graphics g = Graphics.FromImage((System.Drawing.Image)result))
        {
            var box = PlaceInside(oldWidth, oldHeight, nWidth, nHeight);
            g.DrawImage(b, box);
        }
        return result;
    }

Update 2

And here's a version that creates an image of width 200 and proportional height if landscape and height 200 and proportional width if portrait:

    private static Bitmap ResizeBitmapUpto(Bitmap b, int nWidth, int nHeight, System.Drawing.Drawing2D.InterpolationMode interpolationMode)
    {
        int oldWidth = b.Width;
        int oldHeight = b.Height;
        var box = PlaceInside(oldWidth, oldHeight, nWidth, nHeight);
        int actualNewWidth = (int)Math.Max(Math.Round(box.Width), 1);
        int actualNewHeight = (int)Math.Max(Math.Round(box.Height), 1);
        Bitmap result = new Bitmap(actualNewWidth, actualNewHeight);
        using (Graphics g = Graphics.FromImage((System.Drawing.Image)result))
        {
            g.InterpolationMode = interpolationMode;
            g.DrawImage(b, 0, 0, actualNewWidth, actualNewHeight);
        }
        return result;
    }

I added an interpolationMode so you can experiment with different qualities as per Ksv3n's answer.

(hopefully) Last Update

Here's the test setup I used to validate the code. I was able to open, resize and save a variety of images successfully on my computer.

    public static void TestResizeBitmapUpto(string file, string newFile)
    {
        try
        {
            using (var image = Bitmap.FromFile(file))
            {
                if (image == null)
                    return;
                using (Bitmap b = new Bitmap(image))
                {
                    using (var newBitmap = ResizeBitmapUpto(b, 200, 200, System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor))
                    {
                        newBitmap.Save(newFile);
                    }
                }
            }
        }
        catch (System.IO.FileNotFoundException e)
        {
            Debug.WriteLine(e.ToString());
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.ToString());
        }
    }

What's missing in your code is :

   g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

Here is the method you can use to resize your image with keeeping it proportional :

   private Bitmap ResizeBitmap(Bitmap b, int nWidth, int nHeight)
    {
        Bitmap result = new Bitmap(nWidth, nHeight);
        using (Graphics g = Graphics.FromImage((System.Drawing.Image)result))
         {
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
            g.DrawImage(b, 0, 0, nWidth, nHeight);
         }
        return result;
    }

g.DrawImage will stretch the image to what you defined (in your case 200/200 which is a square).

You need to calculate the real values for nWidth and nHeight:

// original image (b.Width, b.Height)
double originalWidth = 200;
double originalHeight = 100;

// user defined wanted width
double wantedWidth = 200;  // nWidth parameter to your method
double wantedHeight = 300; // nHeight parameter to your method

double ratioW = originalWidth / wantedWidth;
double ratioH = originalHeight / wantedHeight;

double ratio = Math.Max(ratioW, ratioH);

// rectangle proportional to the original that fits into the wanted
double destinationWidth = originalWidth / ratio;  // what you pass to DrawImage as nWidth
double destinationHeight = originalHeight / ratio;  // what you pass to DrawImage as nWidth

What it does is calculates the width and height ratio of the original and wanted image and takes the max of it. Uses that to divide the original values, which will make them fit perfectly inside the wanted rectangle.

This will draw the scaled image aligned top or left depending what is the orientation, since the resulting image will be equal or bigger then the wanted or original. To make it centered in the resulting image you would need to adjust left and top coordinates for DrawImage() by taking the difference in width/height and dividing by 2.

If the resulting image can be of different size then what the user specified (nWidth/nHeight) then you can simply initialize it with the destinationWidth/Height, and return that instead, without bothering for centering.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM