0

The goal is to have a control providing the user with the ability to translate/scale/rotate images and with following specs:

  1. Operations undoable
  2. Scale and rotation should cause a new image (outputImage) to be created (so I cannot simply set Graphics.Transform on the OnPaint); translation should not to avoid unnecessary redrawings
  3. The outputImage should be calculated starting from the original image to avoid quality degradation (if an image is firstly scaled down and then up again, the quality should be as at the beginning)

So far, I came to the following code:

public partial class ImageControl : UserControl
{
    Image image, outputImage;
    PointF offset = PointF.Empty;
    Stack<Matrix> transformStack = new Stack<Matrix>();
    public ImageControl() { InitializeComponent(); }
    public Image Image { get { return image; } set { image = outputImage = value; Restore(); } }
    public void Translate(float dx, float dy)
    {
        transformStack.Push(new Matrix(1, 0, 0, 1, dx, dy));
        ApplyTransform(true);
    }
    public void Scale(float scaleX, float scaleY)
    {
        // as an example we scale at the top-left corner
        Matrix m = new Matrix(scaleX, 0, 0, scaleY, offset.X - scaleX * offset.X, offset.Y - scaleY * offset.Y);
        transformStack.Push(m);
        ApplyTransform();
    }
    public void Rotate(float angleDegrees)
    {
        Matrix m = new Matrix();
        // as an example we rotate around the centre of the image
        Point[] pts = new Point[] { new Point(0, 0), new Point(image.Width, 0), new Point(image.Width, image.Height), new Point(0, image.Height) };
        GetTransform().VectorTransformPoints(pts);
        var centre = PolygonCentroid(pts);
        m.RotateAt(angleDegrees, new PointF(offset.X + centre.X, offset.Y + centre.Y));

        transformStack.Push(m);
        ApplyTransform();
    }
    public void Restore()
    {
        offset = PointF.Empty;
        transformStack = new Stack<Matrix>();
        ApplyTransform();
    }
    public void Undo()
    {
        if(transformStack.Count != 0)
            transformStack.Pop();
        ApplyTransform();
    }
    Matrix GetTransform()
    {
        Matrix m = new Matrix();
        foreach (var item in transformStack.Reverse())
            m.Multiply(item, MatrixOrder.Append);
        return m;
    }
    void ApplyTransform(bool onlyTranslation = false)
    {
        Matrix transform = GetTransform();
        if (!onlyTranslation) // we do not need to redraw the image if transformation is pure translation
        {
            // transform the 4 vertices to know the output size
            PointF[] pts = new PointF[] { new PointF(0, 0), new PointF(image.Width, 0), new PointF(0, image.Height), new PointF(image.Width, image.Height) };
            transform.TransformPoints(pts);
            float minX = pts.Min(p => p.X);
            float maxX = pts.Max(p => p.X);
            float minY = pts.Min(p => p.Y);
            float maxY = pts.Max(p => p.Y);
            Bitmap bmpDest = new Bitmap(Convert.ToInt32(maxX - minX), Convert.ToInt32(maxY - minY));
            //bmpDest.SetResolution(image.HorizontalResolution, image.VerticalResolution);

            // remove the offset from the points defining the destination for the image (we need only 3 vertices)
            PointF[] destPts = new PointF[] { new PointF(pts[0].X - minX, pts[0].Y - minY), new PointF(pts[1].X - minX, pts[1].Y - minY), new PointF(pts[2].X - minX, pts[2].Y - minY) };
            using (Graphics gDest = Graphics.FromImage(bmpDest))
                gDest.DrawImage(image, destPts);
            outputImage = bmpDest; 
        }
        // keep the offset
        offset = new PointF(transform.OffsetX, transform.OffsetY);
        Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (image == null) return;

        e.Graphics.TranslateTransform(offset.X, offset.Y);
        e.Graphics.DrawImage(outputImage, 0, 0);
        e.Graphics.DrawRectangle(Pens.DeepSkyBlue, 0, 0, outputImage.Width, outputImage.Height);
    }
}

which (at least) has a problem with rotation: I cannot rotate around the real centre of the image. The function PolygonCentroid is taken from here and should be fine. I guess the error is related to the new offset computed after rotation, for which maybe I should introduce some compensation. Any idea?

Community
  • 1
  • 1
Mauro Ganswer
  • 1,379
  • 1
  • 19
  • 33

1 Answers1

0

I had a misconception on the effect of applying rotation, which was creating the displacement. For those interested:

 public void Rotate(float angleDegrees)
    {
        Matrix m = new Matrix();
        // as an example we rotate around the centre of the image
        Point[] pts = new Point[] { new Point(0, 0), new Point(image.Width, 0), new Point(image.Width, image.Height), new Point(0, image.Height) };
        GetTransform().TransformPoints(pts);
        var centre = PolygonCentroid(pts);
        m.RotateAt(angleDegrees, new PointF(centre.X, centre.Y));

        transformStack.Push(m);
        ApplyTransform();
    }

and the offset used to translate Graphics before painting the outputImage, should be new PointF(minX, minY).

Mauro Ganswer
  • 1,379
  • 1
  • 19
  • 33