Search code examples
c#silverlightwindows-phone-7rotationwindows-phone

Silverlight Rotate & Scale a bitmap image to fit within rectangle without cropping


I need to rotate a WriteableBitmap and scale it down or up before it gets cropped.

My current code will rotate but will crop the edges if the height is larger then the width.

I assume I need to scale?

 public WriteableBitmap Rotate(WriteableBitmap Source, double Angle)
        {
            RotateTransform rt = new RotateTransform();
            rt.Angle = Angle;

            TransformGroup transform = new TransformGroup();
            transform.Children.Add(rt);

            Image tempImage2 = new Image();
            WriteableBitmap wb;
            rt.CenterX = Source.PixelWidth / 2;
            rt.CenterY = Source.PixelHeight / 2;
            tempImage2.Width = Source.PixelWidth;
            tempImage2.Height = Source.PixelHeight;
            wb = new WriteableBitmap((int)(Source.PixelWidth), Source.PixelHeight);
            tempImage2.Source = Source;
            tempImage2.UpdateLayout();

            wb.Render(tempImage2, transform);
            wb.Invalidate();

            return wb;

        }

How do I scale down the image so it will not be cropped? Or is there another way?


Solution

  • You need to calculate the scaling based on the rotation of the corners relative to the centre.

    If the image is a square only one corner is needed, but for a rectangle you need to check 2 corners in order to see if a vertical or horizontal edge is overlapped. This check is a linear comparison of how much the rectangle's height and width are exceeded.

    Click here for the working testbed app created for this answer (image below): (apologies, all my website content was lost thanks to a non-awesome hosting company)

    enter image description here

    double CalculateConstraintScale(double rotation, int pixelWidth, int pixelHeight)
    

    The pseudo-code is as follows (actual C# code at the end):

    • Convert rotation angle into Radians
    • Calculate the "radius" from the rectangle centre to a corner
    • Convert BR corner position to polar coordinates
    • Convert BL corner position to polar coordinates
    • Apply the rotation to both polar coordinates
    • Convert the new positions back to Cartesian coordinates (ABS value)
    • Find the largest of the 2 horizontal positions
    • Find the largest of the 2 vertical positions
    • Calculate the delta change for horizontal size
    • Calculate the delta change for vertical size
    • Return width/2 / x if horizontal change is greater
    • Return height/2 / y if vertical change is greater

    The result is a multiplier that will scale the image down to fit the original rectangle regardless of rotation.

    *Note: While it is possible to do much of the maths using matrix operations, there are not enough calculations to warrant that. I also thought it would make a better example from first-principles.

    C# Code:

        /// <summary>
        /// Calculate the scaling required to fit a rectangle into a rotation of that same rectangle
        /// </summary>
        /// <param name="rotation">Rotation in degrees</param>
        /// <param name="pixelWidth">Width in pixels</param>
        /// <param name="pixelHeight">Height in pixels</param>
        /// <returns>A scaling value between 1 and 0</returns>
        /// <remarks>Released to the public domain 2011 - David Johnston (HiTech Magic Ltd)</remarks>
        private double CalculateConstraintScale(double rotation, int pixelWidth, int pixelHeight)
        {
            // Convert angle to radians for the math lib
            double rotationRadians = rotation * PiDiv180;
    
            // Centre is half the width and height
            double width = pixelWidth / 2.0;
            double height = pixelHeight / 2.0;
            double radius = Math.Sqrt(width * width + height * height);
    
            // Convert BR corner into polar coordinates
            double angle = Math.Atan(height / width);
    
            // Now create the matching BL corner in polar coordinates
            double angle2 = Math.Atan(height / -width);
    
            // Apply the rotation to the points
            angle += rotationRadians;
            angle2 += rotationRadians;
    
            // Convert back to rectangular coordinate
            double x = Math.Abs(radius * Math.Cos(angle));
            double y = Math.Abs(radius * Math.Sin(angle));
            double x2 = Math.Abs(radius * Math.Cos(angle2));
            double y2 = Math.Abs(radius * Math.Sin(angle2));
    
            // Find the largest extents in X & Y
            x = Math.Max(x, x2);
            y = Math.Max(y, y2);
    
            // Find the largest change (pixel, not ratio)
            double deltaX = x - width;
            double deltaY = y - height;
    
            // Return the ratio that will bring the largest change into the region
            return (deltaX > deltaY) ? width / x : height / y;
        }
    

    Example of use:

        private WriteableBitmap GenerateConstrainedBitmap(BitmapImage sourceImage, int pixelWidth, int pixelHeight, double rotation)
        {
            double scale = CalculateConstraintScale(rotation, pixelWidth, pixelHeight);
    
            // Create a transform to render the image rotated and scaled
            var transform = new TransformGroup();
            var rt = new RotateTransform()
                {
                    Angle = rotation,
                    CenterX = (pixelWidth / 2.0),
                    CenterY = (pixelHeight / 2.0)
                };
            transform.Children.Add(rt);
            var st = new ScaleTransform()
                {
                    ScaleX = scale,
                    ScaleY = scale,
                    CenterX = (pixelWidth / 2.0),
                    CenterY = (pixelHeight / 2.0)
                };
            transform.Children.Add(st);
    
            // Resize to specified target size
            var tempImage = new Image()
                {
                    Stretch = Stretch.Fill,
                    Width = pixelWidth,
                    Height = pixelHeight,
                    Source = sourceImage,
                };
            tempImage.UpdateLayout();
    
            // Render to a writeable bitmap
            var writeableBitmap = new WriteableBitmap(pixelWidth, pixelHeight);
            writeableBitmap.Render(tempImage, transform);
            writeableBitmap.Invalidate();
            return writeableBitmap;
        }
    

    I released a Test-bed of the code on my website so you can try it for real - click to try it (apologies, all my website content was lost thanks to a non-awesome hosting company)