The goal is to have a control providing the user with the ability to translate/scale/rotate images and with following specs:
- Operations undoable
- Scale and rotation should cause a new image (
outputImage
) to be created (so I cannot simply setGraphics.Transform
on theOnPaint
); translation should not to avoid unnecessary redrawings - 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?