feat: add mosaic screenshot annotations

This commit is contained in:
2026-07-12 20:40:05 +08:00
parent dd12521be2
commit b82108db38
4 changed files with 535 additions and 21 deletions
+91
View File
@@ -81,6 +81,10 @@ func ApplyAnnotationsWithScale(pngBytes []byte, annotations []Annotation, scaleX
drawEllipseOutline(dst, ann.Points, scaleX, scaleY, width, c)
case "text":
drawTextAnnotation(dst, ann, scaleX, scaleY, c)
case "mosaic-brush":
applyMosaicBrush(dst, ann.Points, scaleX, scaleY, width)
case "mosaic-rect":
applyMosaicRect(dst, ann.Points, scaleX, scaleY)
}
}
@@ -91,6 +95,93 @@ func ApplyAnnotationsWithScale(pngBytes []byte, annotations []Annotation, scaleX
return buf.Bytes(), nil
}
const mosaicBlockSize = 10
func applyMosaicBrush(img *image.RGBA, points []Point, scaleX, scaleY float64, width int) {
if len(points) == 0 {
return
}
mask := image.NewAlpha(img.Bounds())
maskColor := color.RGBA{A: 255}
if len(points) == 1 {
drawDotMask(mask, scalePoint(points[0], scaleX, scaleY), width, maskColor)
} else {
for i := 1; i < len(points); i++ {
drawMaskLine(mask, scalePoint(points[i-1], scaleX, scaleY), scalePoint(points[i], scaleX, scaleY), width, maskColor)
}
}
applyPixelation(img, img.Bounds(), mask)
}
func applyMosaicRect(img *image.RGBA, points []Point, scaleX, scaleY float64) {
if len(points) < 2 {
return
}
a := scalePoint(points[0], scaleX, scaleY)
b := scalePoint(points[len(points)-1], scaleX, scaleY)
x1, x2 := ordered(a.X, b.X)
y1, y2 := ordered(a.Y, b.Y)
r := image.Rect(int(math.Floor(x1)), int(math.Floor(y1)), int(math.Ceil(x2)), int(math.Ceil(y2))).Intersect(img.Bounds())
applyPixelation(img, r, nil)
}
func applyPixelation(img *image.RGBA, region image.Rectangle, mask *image.Alpha) {
if region.Empty() {
return
}
for y := region.Min.Y; y < region.Max.Y; y += mosaicBlockSize {
for x := region.Min.X; x < region.Max.X; x += mosaicBlockSize {
block := image.Rect(x, y, min(x+mosaicBlockSize, region.Max.X), min(y+mosaicBlockSize, region.Max.Y))
var r, g, b, a, count uint64
for py := block.Min.Y; py < block.Max.Y; py++ {
for px := block.Min.X; px < block.Max.X; px++ {
c := img.RGBAAt(px, py)
r += uint64(c.R)
g += uint64(c.G)
b += uint64(c.B)
a += uint64(c.A)
count++
}
}
if count == 0 {
continue
}
avg := color.RGBA{uint8(r / count), uint8(g / count), uint8(b / count), uint8(a / count)}
for py := block.Min.Y; py < block.Max.Y; py++ {
for px := block.Min.X; px < block.Max.X; px++ {
if mask == nil || mask.AlphaAt(px, py).A > 0 {
img.SetRGBA(px, py, avg)
}
}
}
}
}
}
func drawMaskLine(mask *image.Alpha, a, b Point, width int, c color.RGBA) {
dx, dy := b.X-a.X, b.Y-a.Y
steps := int(math.Max(math.Abs(dx), math.Abs(dy)))
if steps == 0 {
drawDotMask(mask, a, width, c)
return
}
for i := 0; i <= steps; i++ {
t := float64(i) / float64(steps)
drawDotMask(mask, Point{X: a.X + dx*t, Y: a.Y + dy*t}, width, c)
}
}
func drawDotMask(mask *image.Alpha, p Point, width int, c color.RGBA) {
radius := float64(width) / 2
for y := int(math.Floor(p.Y - radius)); y <= int(math.Ceil(p.Y+radius)); y++ {
for x := int(math.Floor(p.X - radius)); x <= int(math.Ceil(p.X+radius)); x++ {
if image.Pt(x, y).In(mask.Bounds()) && math.Hypot(float64(x)-p.X, float64(y)-p.Y) <= radius {
mask.SetAlpha(x, y, color.Alpha{A: c.A})
}
}
}
}
func parseHexColor(hex string) (color.RGBA, error) {
value := strings.TrimPrefix(strings.TrimSpace(hex), "#")
if len(value) != 6 {