Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
504 views
in Technique[技术] by (71.8m points)

.net - How to draw a transparent shape over an Image

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.

enter image description here

Edit:

I generally use Graphics.FromImage(image) when drawing, i.e.

Graphics.FromImage(image).DrawRectangle(...) 

but I want to make a transparent hole or rectangle in the middle of an image.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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:

GraphicsPath Draw Holes

? 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);
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...