How do I draw a shape over an image to overwrite what\'s there and make it transparent?
Like the transparent hole in the middle of the image below.
Edit
This method makes use of two GraphicsPath objects and a TextureBrush to draw transparent holes inside a Bitmap (see the description of this functionality in the Worker methods
part).
When the Bitmap we want to work with is loaded, (here, using File.ReadAllBytes() and a MemoryStream
to avoid locking the image file on disk), it's assigned to a private Field, drawingBitmap
which is then cloned to create the object shown in a PictureBox.Image
property (the original Image is always duplicated in a way or another, we never modify it).
► The selectionRect
Field keeps track of the area selected (with different means, as shown in the visual sample).
► The shapeOfHole
Field is an Enumerator that specifies the type of the shape that selectionRect
is describing (here, a Rectangle or an Ellipse, but it could be any other shape: using GraphicsPaths as containers makes it even simpler to add polygon shapes).
► The preserveImage
boolean Field is a selector used to determine whether the new holes are added to the existing Image or a new hole is created each time.
In the sample code here, two Buttons, btnLoadImage
and btnPaintHole
are used to activate the main functions (loading and assigning the Image and drawing one or more holes in the selected Bitmap).
picCanvas
is the PictureBox used to show the Image.
Private drawingBitmap As Image = Nothing
Private selectionRect As RectangleF = New RectangleF(100, 100, 50, 50)
Private shapeOfHole As ShapeType = ShapeType.Rectangle
Private preserveImage as Boolean = False
Private Sub btnLoadImage_Click(sender As Object, e As EventArgs) Handles btnLoadImage.Click
Dim imagePath As String = [Your Image Path]
drawingBitmap = Image.FromStream(New MemoryStream(File.ReadAllBytes(imagePath)))
picCanvas.Image?.Dispose()
picCanvas.Image = DirectCast(drawingBitmap.Clone(), Bitmap)
End Sub
Private Sub btnPaintHole_Click(sender As Object, e As EventArgs) Handles btnPaintHole.Click
Dim newImage As Image = Nothing
If preserveImage AndAlso picCanvas.Image IsNot Nothing Then
newImage = DrawHole(picCanvas.Image, picCanvas, selectionRect, shapeOfHole)
Else
newImage = DrawHole(drawingBitmap, picCanvas, selectionRect, shapeOfHole)
End If
If newImage IsNot Nothing Then
picCanvas.Image?.Dispose()
picCanvas.Image = newImage
End If
End Sub
Visual sample of the functionality:
► The Image used as the PictureBox.BackgroundImage to simulate the classic transparent background .
Worker methods:
► The DrawHole()
method uses two GraphicsPath objects.
The imagePath
object is sized as the original Image, the selectionPath
object is sized as the current selection area (will be scaled to match the Image real size after).
Using the FillMode.Alternate mode, the imagePath.AddPath(selectionPath, True) method sets the connect
argument to True
, specifying that the added selectionPath
becomes part of imagePath
. Since FillMode.Alternate
is an XOR operation, we create a hole in imagePath
.
The Graphics.FillPath() method then uses a TextureBrush to fill the GraphicsPath, except the XOR-ed part, with the Bitmap object, which will then contain an anti-aliased transparent area (the Graphics object uses the SmoothingMode.AntiAlias
mode).
► The GetScaledSelectionRect()
method uses a trick to simplify the calculation of the unscaled coordinates of the selection Rectangle inside a scaled Image (the PictureBox Control SizeMode
is most probably set to PictureBoxSizeMode.Zoom
): it reads the .Net PictureBox class ImageRectangle property (who knows why, private
), to determine the Image scaled bounds and calculates the offset and scale of the selection rectangle based on this measure.
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Reflection
Friend Enum ShapeType
Rectangle
Ellipse
End Enum
Friend Function DrawHole(srcImage As Image, canvas As PictureBox, holeShape As RectangleF, typeOfShape As ShapeType) As Image
Dim cropped = New Bitmap(srcImage.Width, srcImage.Height, PixelFormat.Format32bppArgb)
Dim imageRect = New RectangleF(Point.Empty, srcImage.Size)
Dim selectionRect = GetScaledSelectionRect(canvas, holeShape)
Using tBrush = New TextureBrush(srcImage),
imagePath = New GraphicsPath(FillMode.Alternate),
selectionPath = New GraphicsPath(),
g = Graphics.FromImage(cropped)
Select Case typeOfShape
Case ShapeType.Ellipse
selectionPath.AddEllipse(selectionRect)
Case ShapeType.Rectangle
selectionPath.AddRectangle(selectionRect)
End Select
imagePath.AddRectangle(imageRect)
imagePath.AddPath(selectionPath, True)
g.SmoothingMode = SmoothingMode.AntiAlias
g.FillPath(tBrush, imagePath)
Return cropped
End Using
End Function
Friend Function GetScaledSelectionRect(canvas As PictureBox, selectionRect As RectangleF) As RectangleF
If canvas.Image Is Nothing Then Return selectionRect
Dim flags = BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.GetProperty
Dim imageRect = DirectCast(canvas.GetType().GetProperty("ImageRectangle", flags).GetValue(canvas), Rectangle)
Dim scaleX = CSng(canvas.Image.Width) / imageRect.Width
Dim scaleY = CSng(canvas.Image.Height) / imageRect.Height
Dim selectionOffset = RectangleF.Intersect(imageRect, selectionRect)
selectionOffset.Offset(-imageRect.X, -imageRect.Y)
Return New RectangleF(selectionOffset.X * scaleX, selectionOffset.Y * scaleY,
selectionOffset.Width * scaleX, selectionOffset.Height * scaleY)
End Function
C# version:
private Image drawingBitmap = null;
private RectangleF selectionRect = new RectangleF(100, 100, 50, 50);
private ShapeType shapeOfHole = ShapeType.Rectangle;
private bool preserveImage = false;
private void btnLoadImage_Click(object sender, EventArgs e)
{
string imagePath = [Your Image Path];
drawingBitmap = Image.FromStream(new MemoryStream(File.ReadAllBytes(imagePath)));
picCanvas.Image?.Dispose();
picCanvas.Image = drawingBitmap.Clone() as Bitmap;
}
private void btnPaintHole_Click(object sender, EventArgs e)
{
Image newImage = null;
if (preserveImage && picCanvas.Image != null) {
newImage = DrawHole(picCanvas.Image, picCanvas, selectionRect, shapeOfHole);
}
else {
newImage = DrawHole(drawingBitmap, picCanvas, selectionRect, shapeOfHole);
}
if (newImage != null) {
picCanvas.Image?.Dispose();
picCanvas.Image = newImage;
}
}
Worker methods:
Note: GetScaledSelectionRect()
, as described, uses Reflection to read the PictureBox private
ImageRectangle
property from the .Net control.
Since this method is called from the drawing procedure, it's probably better to re-implement this method in a custom PictureBox control, or perform the calculations without invoking the underlying method (reflection is not as slow as sometimes advertised, but it's of course slower than using some math directly, here).
Some possible implementations are shown (for example) here:
Zoom and translate an Image from the mouse location
Translate Rectangle Position in a Picturebox with SizeMode.Zoom
internal enum ShapeType {
Rectangle,
Ellipse
}
internal Image DrawHole(Image srcImage, PictureBox canvas, RectangleF holeShape, ShapeType typeOfShape)
{
var cropped = new Bitmap(srcImage.Width, srcImage.Height, PixelFormat.Format32bppArgb);
var imageRect = new RectangleF(Point.Empty, srcImage.Size);
RectangleF selectionRect = GetScaledSelectionRect(canvas, holeShape);
using (var tBrush = new TextureBrush(srcImage))
using (var imagePath = new GraphicsPath(FillMode.Alternate))
using (var selectionPath = new GraphicsPath())
using (var g = Graphics.FromImage(cropped)) {
switch (typeOfShape) {
case ShapeType.Ellipse:
selectionPath.AddEllipse(selectionRect);
break;
case ShapeType.Rectangle:
selectionPath.AddRectangle(selectionRect);
break;
}
imagePath.AddRectangle(imageRect);
imagePath.AddPath(selectionPath, true);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillPath(tBrush, imagePath);
return cropped;
}
}
internal RectangleF GetScaledSelectionRect(PictureBox canvas, RectangleF selectionRect)
{
if (canvas.Image == null) return selectionRect;
var flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty;
var imageRect = (Rectangle)canvas.GetType().GetProperty("ImageRectangle", flags).GetValue(canvas);
var scaleX = (float)canvas.Image.Width / imageRect.Width;
var scaleY = (float)canvas.Image.Height / imageRect.Height;
var selectionOffset = RectangleF.Intersect(imageRect, selectionRect);
selectionOffset.Offset(-imageRect.X, -imageRect.Y);
return new RectangleF(selectionOffset.X * scaleX, selectionOffset.Y * scaleY,
selectionOffset.Width * scaleX, selectionOffset.Height * scaleY);
}