How to draw a transparent shape over an Image

后端 未结 1 424
深忆病人
深忆病人 2021-01-20 22:18

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

相关标签:
1条回答
  • 2021-01-20 22:50

    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);
    }
    
    0 讨论(0)
提交回复
热议问题