diff --git a/resources/images/filters.go b/resources/images/filters.go index 99fecdf20..64776affe 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -52,6 +52,14 @@ func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter { } } +// Mask creates a filter that applies a mask image to the source image. +func (*Filters) Mask(mask ImageSource) gift.Filter { + return filter{ + Options: newFilterOpts(mask.Key()), + Filter: maskFilter{mask: mask}, + } +} + // Opacity creates a filter that changes the opacity of an image. // The opacity parameter must be in range (0, 1). func (*Filters) Opacity(opacity any) gift.Filter { diff --git a/resources/images/mask.go b/resources/images/mask.go new file mode 100644 index 000000000..5ce7c5d43 --- /dev/null +++ b/resources/images/mask.go @@ -0,0 +1,63 @@ +package images + +import ( + "fmt" + "image" + "image/color" + "image/draw" + + "github.com/disintegration/gift" +) + +// maskFilter applies a mask image to a base image. +type maskFilter struct { + mask ImageSource +} + +// Draw applies the mask to the base image. +func (f maskFilter) Draw(dst draw.Image, baseImage image.Image, options *gift.Options) { + maskImage, err := f.mask.DecodeImage() + if err != nil { + panic(fmt.Sprintf("failed to decode image: %s", err)) + } + + // Ensure the mask is the same size as the base image + baseBounds := baseImage.Bounds() + maskBounds := maskImage.Bounds() + + // Resize mask to match base image size if necessary + if maskBounds.Dx() != baseBounds.Dx() || maskBounds.Dy() != baseBounds.Dy() { + g := gift.New(gift.Resize(baseBounds.Dx(), baseBounds.Dy(), gift.LanczosResampling)) + resizedMask := image.NewRGBA(g.Bounds(maskImage.Bounds())) + g.Draw(resizedMask, maskImage) + maskImage = resizedMask + } + + // Use gift to convert the resized mask to grayscale + g := gift.New(gift.Grayscale()) + grayscaleMask := image.NewGray(g.Bounds(maskImage.Bounds())) + g.Draw(grayscaleMask, maskImage) + + // Convert grayscale mask to alpha mask + alphaMask := image.NewAlpha(baseBounds) + for y := baseBounds.Min.Y; y < baseBounds.Max.Y; y++ { + for x := baseBounds.Min.X; x < baseBounds.Max.X; x++ { + grayValue := grayscaleMask.GrayAt(x, y).Y + alphaMask.SetAlpha(x, y, color.Alpha{A: grayValue}) + } + } + + // Create an RGBA output image + outputImage := image.NewRGBA(baseBounds) + + // Apply the mask using draw.DrawMask + draw.DrawMask(outputImage, baseBounds, baseImage, image.Point{}, alphaMask, image.Point{}, draw.Over) + + // Copy the result to the destination + gift.New().Draw(dst, outputImage) +} + +// Bounds returns the bounds of the resulting image. +func (f maskFilter) Bounds(imgBounds image.Rectangle) image.Rectangle { + return image.Rect(0, 0, imgBounds.Dx(), imgBounds.Dy()) +}