feat: enhance watermarking with rotation, tiling, and GIF support (by AI)
This commit is contained in:
parent
aa87dcc6ce
commit
762370e839
@ -1,5 +1,12 @@
|
|||||||
# CHANGELOG - apigo.cc/go/vision
|
# CHANGELOG - apigo.cc/go/vision
|
||||||
|
|
||||||
|
## v1.0.5 (2026-05-13)
|
||||||
|
- **高级水印系统**:
|
||||||
|
- 为 `Watermark` 和 `TextWatermark` 增加旋转角度 (`angle`) 支持。
|
||||||
|
- 新增 `TileWatermark` 和 `TileTextWatermark` 实现全图平铺水印,支持自定义间距与角度。
|
||||||
|
- **GIF 水印支持**: 为 `Animation` 结构增加全套水印方法,支持对动图所有帧批量添加水印。
|
||||||
|
- **状态确认**: 确认并完善了二维码 (`QR Code`) 与条形码 (`Barcode`) 的生成与识别能力。
|
||||||
|
|
||||||
## v1.0.4 (2026-05-13)
|
## v1.0.4 (2026-05-13)
|
||||||
- **水印系统**: 新增 `Watermark` (图片) 和 `TextWatermark` (文字) 支持九宫格位置定义与透明度。
|
- **水印系统**: 新增 `Watermark` (图片) 和 `TextWatermark` (文字) 支持九宫格位置定义与透明度。
|
||||||
- **视频水印**: 扩展 `Video` 结构,支持通过 FFmpeg 一键给视频添加水印。
|
- **视频水印**: 扩展 `Video` 结构,支持通过 FFmpeg 一键给视频添加水印。
|
||||||
|
|||||||
28
animation.go
28
animation.go
@ -83,3 +83,31 @@ func LoadGIF(path string) (*Animation, error) {
|
|||||||
}
|
}
|
||||||
return anim, nil
|
return anim, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watermark 给动画所有帧添加图片水印
|
||||||
|
func (a *Animation) Watermark(mark *Canvas, pos Position, opacity float64, padding int, angle ...float64) {
|
||||||
|
for _, f := range a.Frames {
|
||||||
|
f.Watermark(mark, pos, opacity, padding, angle...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TileWatermark 给动画所有帧平铺图片水印
|
||||||
|
func (a *Animation) TileWatermark(mark *Canvas, opacity float64, spacing int, angle float64) {
|
||||||
|
for _, f := range a.Frames {
|
||||||
|
f.TileWatermark(mark, opacity, spacing, angle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextWatermark 给动画所有帧添加文字水印
|
||||||
|
func (a *Animation) TextWatermark(text string, pos Position, style *DrawStyle, opacity float64, padding int, angle ...float64) {
|
||||||
|
for _, f := range a.Frames {
|
||||||
|
f.TextWatermark(text, pos, style, opacity, padding, angle...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TileTextWatermark 给动画所有帧平铺文字水印
|
||||||
|
func (a *Animation) TileTextWatermark(text string, style *DrawStyle, opacity float64, spacing int, angle float64) {
|
||||||
|
for _, f := range a.Frames {
|
||||||
|
f.TileTextWatermark(text, style, opacity, spacing, angle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -65,6 +65,36 @@ func TestPHash(t *testing.T) {
|
|||||||
t.Logf("pHash distance: %d", dist)
|
t.Logf("pHash distance: %d", dist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTileWatermark(t *testing.T) {
|
||||||
|
c := New(400, 300, "#FFFFFF")
|
||||||
|
c.Circle(200, 150, 50, &DrawStyle{FillColor: "#0000FF"})
|
||||||
|
|
||||||
|
c.TileTextWatermark("CONFIDENTIAL", &DrawStyle{FillColor: "#FF0000"}, 0.2, 50, -0.785) // 45度
|
||||||
|
|
||||||
|
err := Save(c, "test_tile_watermark.png")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("save tile watermark test failed: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove("test_tile_watermark.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimationWatermark(t *testing.T) {
|
||||||
|
anim := NewAnimation()
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
c := New(100, 100, "#FFFFFF")
|
||||||
|
c.Circle(float64(i*20), 50, 10, &DrawStyle{FillColor: "#00FF00"})
|
||||||
|
anim.AddFrame(c, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.TextWatermark("GO", Center, &DrawStyle{FillColor: "#000000"}, 0.5, 0)
|
||||||
|
|
||||||
|
err := anim.SaveGIF("test_anim_watermark.gif", 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("save anim watermark failed: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove("test_anim_watermark.gif")
|
||||||
|
}
|
||||||
|
|
||||||
func TestQRCode(t *testing.T) {
|
func TestQRCode(t *testing.T) {
|
||||||
content := "https://apigo.cc"
|
content := "https://apigo.cc"
|
||||||
c, err := GenerateQRCode(content, 200)
|
c, err := GenerateQRCode(content, 200)
|
||||||
|
|||||||
76
watermark.go
76
watermark.go
@ -26,7 +26,8 @@ const (
|
|||||||
// pos: 位置
|
// pos: 位置
|
||||||
// opacity: 透明度 (0.0 - 1.0)
|
// opacity: 透明度 (0.0 - 1.0)
|
||||||
// padding: 边距
|
// padding: 边距
|
||||||
func (c *Canvas) Watermark(mark *Canvas, pos Position, opacity float64, padding int) {
|
// angle: 旋转角度 (弧度)
|
||||||
|
func (c *Canvas) Watermark(mark *Canvas, pos Position, opacity float64, padding int, angle ...float64) {
|
||||||
if mark == nil || opacity < 0.01 {
|
if mark == nil || opacity < 0.01 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -56,6 +57,11 @@ func (c *Canvas) Watermark(mark *Canvas, pos Position, opacity float64, padding
|
|||||||
x, y = w-mw-padding, h-mh-padding
|
x, y = w-mw-padding, h-mh-padding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.dc.Push()
|
||||||
|
if len(angle) > 0 && angle[0] != 0 {
|
||||||
|
c.dc.RotateAbout(angle[0], float64(x+mw/2), float64(y+mh/2))
|
||||||
|
}
|
||||||
|
|
||||||
// 处理透明度
|
// 处理透明度
|
||||||
if opacity >= 0.99 {
|
if opacity >= 0.99 {
|
||||||
c.dc.DrawImage(mark.dc.Image(), x, y)
|
c.dc.DrawImage(mark.dc.Image(), x, y)
|
||||||
@ -63,23 +69,71 @@ func (c *Canvas) Watermark(mark *Canvas, pos Position, opacity float64, padding
|
|||||||
mask := image.NewUniform(color.Alpha{uint8(255 * opacity)})
|
mask := image.NewUniform(color.Alpha{uint8(255 * opacity)})
|
||||||
draw.DrawMask(c.dc.Image().(draw.Image), mark.dc.Image().Bounds().Add(image.Pt(x, y)), mark.dc.Image(), image.Point{}, mask, image.Point{}, draw.Over)
|
draw.DrawMask(c.dc.Image().(draw.Image), mark.dc.Image().Bounds().Add(image.Pt(x, y)), mark.dc.Image(), image.Point{}, mask, image.Point{}, draw.Over)
|
||||||
}
|
}
|
||||||
|
c.dc.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TileWatermark 平铺图片水印
|
||||||
|
// spacing: 间距
|
||||||
|
// angle: 旋转角度 (弧度)
|
||||||
|
func (c *Canvas) TileWatermark(mark *Canvas, opacity float64, spacing int, angle float64) {
|
||||||
|
if mark == nil || opacity < 0.01 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mw, mh := mark.Width(), mark.Height()
|
||||||
|
w, h := c.Width(), c.Height()
|
||||||
|
|
||||||
|
stepX := mw + spacing
|
||||||
|
stepY := mh + spacing
|
||||||
|
|
||||||
|
for y := -mh; y < h+mh; y += stepY {
|
||||||
|
for x := -mw; x < w+mw; x += stepX {
|
||||||
|
c.dc.Push()
|
||||||
|
c.dc.RotateAbout(angle, float64(x+mw/2), float64(y+mh/2))
|
||||||
|
if opacity >= 0.99 {
|
||||||
|
c.dc.DrawImage(mark.dc.Image(), x, y)
|
||||||
|
} else {
|
||||||
|
mask := image.NewUniform(color.Alpha{uint8(255 * opacity)})
|
||||||
|
draw.DrawMask(c.dc.Image().(draw.Image), mark.dc.Image().Bounds().Add(image.Pt(x, y)), mark.dc.Image(), image.Point{}, mask, image.Point{}, draw.Over)
|
||||||
|
}
|
||||||
|
c.dc.Pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextWatermark 给画布添加文字水印
|
// TextWatermark 给画布添加文字水印
|
||||||
func (c *Canvas) TextWatermark(text string, pos Position, style *DrawStyle, opacity float64, padding int) {
|
func (c *Canvas) TextWatermark(text string, pos Position, style *DrawStyle, opacity float64, padding int, angle ...float64) {
|
||||||
// 创建一个临时画布来渲染文字,然后调用 Watermark
|
// 创建一个临时画布来渲染文字,然后调用 Watermark
|
||||||
// 这样可以利用 Watermark 的位置计算逻辑
|
|
||||||
c.dc.Push()
|
c.dc.Push()
|
||||||
if style != nil && style.FillColor != "" {
|
|
||||||
c.SetColor(style.FillColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里简化实现,实际可以先测量文字宽度
|
|
||||||
tw, th := c.dc.MeasureString(text)
|
tw, th := c.dc.MeasureString(text)
|
||||||
temp := New(int(tw)+2, int(th)+2)
|
temp := New(int(tw)+4, int(th)+4)
|
||||||
|
if style != nil && style.FillColor != "" {
|
||||||
temp.dc.SetColor(ParseColor(style.FillColor))
|
temp.dc.SetColor(ParseColor(style.FillColor))
|
||||||
temp.dc.DrawString(text, 0, th)
|
} else {
|
||||||
|
temp.dc.SetColor(color.Black)
|
||||||
|
}
|
||||||
|
temp.dc.DrawString(text, 2, th+2)
|
||||||
|
|
||||||
c.Watermark(temp, pos, opacity, padding)
|
ang := 0.0
|
||||||
|
if len(angle) > 0 {
|
||||||
|
ang = angle[0]
|
||||||
|
}
|
||||||
|
c.Watermark(temp, pos, opacity, padding, ang)
|
||||||
|
c.dc.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TileTextWatermark 平铺文字水印
|
||||||
|
func (c *Canvas) TileTextWatermark(text string, style *DrawStyle, opacity float64, spacing int, angle float64) {
|
||||||
|
c.dc.Push()
|
||||||
|
tw, th := c.dc.MeasureString(text)
|
||||||
|
temp := New(int(tw)+4, int(th)+4)
|
||||||
|
if style != nil && style.FillColor != "" {
|
||||||
|
temp.dc.SetColor(ParseColor(style.FillColor))
|
||||||
|
} else {
|
||||||
|
temp.dc.SetColor(color.Black)
|
||||||
|
}
|
||||||
|
temp.dc.DrawString(text, 2, th+2)
|
||||||
|
|
||||||
|
c.TileWatermark(temp, opacity, spacing, angle)
|
||||||
c.dc.Pop()
|
c.dc.Pop()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user