This commit is contained in:
Star 2025-07-29 16:59:49 +08:00
commit cc31aa8248
9 changed files with 2750 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.*
!.gitignore
go.sum
package.json
node_modules

847
draw.go Normal file
View File

@ -0,0 +1,847 @@
package img
import (
"fmt"
"image"
"image/color"
"math"
"math/rand"
"unicode"
"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/ssgo/u"
)
type LineCap struct {
Butt string
Round string
Square string
}
var lineCap = LineCap{
Butt: "butt",
Round: "round",
Square: "square",
}
type LineJoin struct {
Bevel string
Miter string
Round string
}
var lineJoin = LineJoin{
Bevel: "bevel",
Round: "round",
}
type FillRule struct {
Winding string
Evenodd string
}
var fillRule = FillRule{
Winding: "winding",
Evenodd: "evenodd",
}
type DrawStyle struct {
LineColor string
LineWidth float64
LineCap string
LineJoin string
Dash []float64
DashOffset float64
FillColor string
FillRule string
ShadowColor string
ShadowOffset float64
ShadowBlur float64
}
func (g *Graphics) draw(fn func(offset float64), opt *DrawStyle) {
if opt == nil {
opt = &DrawStyle{}
}
needFill := opt.FillColor != ""
needStroke := !needFill || opt.LineColor != "" || opt.LineWidth >= 0.01
// 绘制阴影
if opt.ShadowColor != "" || (opt.ShadowOffset >= 0.01 || opt.ShadowBlur <= -0.01) {
if opt.ShadowColor == "" {
opt.ShadowColor = "#333333"
}
if opt.ShadowOffset > -0.01 && opt.ShadowOffset < 0.01 {
opt.ShadowOffset = 2
}
useBlur := opt.ShadowBlur >= 0.01
var tmpdc, olddc *gg.Context
if useBlur {
bounds := g.dc.Image().Bounds()
tmpdc = gg.NewContext(bounds.Dx(), bounds.Dy())
olddc = g.dc
g.dc = tmpdc
}
fn(opt.ShadowOffset)
g.dc.SetColor(hexToRGBA(opt.ShadowColor))
if opt.LineWidth >= 0.01 {
g.dc.SetLineWidth(opt.LineWidth)
}
if needFill {
g.dc.Fill()
} else {
g.dc.Stroke()
}
if needFill {
g.dc.Fill()
} else {
g.dc.Stroke()
}
if useBlur {
g.dc = olddc
tmpimg := tmpdc.Image()
imaging.Blur(tmpimg, opt.ShadowBlur)
g.dc.DrawImage(tmpimg, 0, 0)
}
}
// 绘制图形
fn(0)
if needFill {
g.dc.SetColor(hexToRGBA(opt.FillColor))
if opt.FillRule != "" {
switch opt.FillRule {
case fillRule.Winding:
g.dc.SetFillRule(gg.FillRuleWinding)
case fillRule.Evenodd:
g.dc.SetFillRule(gg.FillRuleEvenOdd)
}
}
if needStroke {
g.dc.FillPreserve()
} else if needFill {
g.dc.Fill()
}
}
if needStroke {
if opt.LineWidth >= 0.01 {
g.dc.SetLineWidth(opt.LineWidth)
needStroke = true
}
if opt.LineCap != "" {
switch opt.LineCap {
case lineCap.Butt:
g.dc.SetLineCap(gg.LineCapButt)
case lineCap.Round:
g.dc.SetLineCap(gg.LineCapRound)
case lineCap.Square:
g.dc.SetLineCap(gg.LineCapSquare)
}
}
if opt.LineJoin != "" {
switch opt.LineJoin {
case lineJoin.Bevel:
g.dc.SetLineJoin(gg.LineJoinBevel)
case lineJoin.Round:
g.dc.SetLineJoin(gg.LineJoinRound)
}
}
if opt.Dash != nil {
g.dc.SetDash(opt.Dash...)
}
if opt.DashOffset >= 0.01 {
g.dc.SetDashOffset(opt.DashOffset)
}
if opt.LineColor != "" {
g.SetColor(opt.LineColor)
} else if g.lastColor != "" {
g.SetColor(g.lastColor)
}
g.dc.Stroke()
if opt.Dash != nil || opt.DashOffset >= 0.01 {
g.dc.SetDash()
}
}
}
// DrawRectangle 绘制矩形
func (g *Graphics) Rect(x, y, w, h float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawRectangle(x+offset, y+offset, w, h)
}, opt)
}
func (g *Graphics) RoundedRect(x, y, w, h, r float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawRoundedRectangle(x+offset, y+offset, w, h, r)
}, opt)
}
// Point 绘制点
func (g *Graphics) Point(x, y float64, c *string) {
if c != nil && *c != "" {
g.SetColor(*c)
}
g.dc.DrawPoint(x, y, 1)
g.dc.Stroke()
}
// Line 绘制直线
func (g *Graphics) Line(x1, y1, x2, y2 float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.MoveTo(x1+offset, y1+offset)
g.dc.LineTo(x2+offset, y2+offset)
}, opt)
}
func convertPoints(points []float64) []gg.Point {
p := make([]gg.Point, 0, len(points)/2)
for i := 0; i < len(points)-1; i += 2 {
p = append(p, gg.Point{X: points[i], Y: points[i+1]})
}
return p
}
// Lines 绘制多条连续线段
func (g *Graphics) Lines(points []float64, opt *DrawStyle) {
pp := convertPoints(points)
g.draw(func(offset float64) {
if len(pp) >= 0 {
g.dc.MoveTo(pp[0].X+offset, pp[0].Y+offset)
for _, p := range pp[1:] {
g.dc.LineTo(p.X+offset, p.Y+offset)
}
}
}, opt)
}
// Circle 绘制圆形
func (g *Graphics) Circle(x, y, r float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawCircle(x+offset, y+offset, r)
}, opt)
}
// Ellipse 绘制椭圆
func (g *Graphics) Ellipse(x, y, rx, ry float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawEllipse(x+offset, y+offset, rx, ry)
}, opt)
}
// Arc 绘制圆弧
func (g *Graphics) Arc(x, y, r, angle1, angle2 float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawArc(x+offset, y+offset, r, angle1, angle2)
}, opt)
}
// Sector 绘制扇形
func (g *Graphics) Sector(x, y, r, angle1, angle2 float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.DrawArc(x+offset, y+offset, r, angle1, angle2)
g.dc.LineTo(x+offset, y+offset)
g.dc.LineTo(x+offset+r*math.Cos(angle1), y+offset+r*math.Sin(angle1))
}, opt)
}
// Ring 绘制圆环
func (g *Graphics) Ring(x, y, innerRadius, outerRadius float64, opt *DrawStyle) {
g.draw(func(offset float64) {
// 绘制外圆
g.dc.DrawCircle(x+offset, y+offset, outerRadius)
// 绘制内圆(作为"洞"
g.dc.DrawCircle(x+offset, y+offset, innerRadius)
g.dc.SetFillRule(gg.FillRuleEvenOdd)
}, opt)
}
// ArcRing 绘制扇形环
func (g *Graphics) ArcRing(x, y, innerRadius, outerRadius, angle1, angle2 float64, opt *DrawStyle) {
g.draw(func(offset float64) {
// 绘制外圆弧
g.dc.DrawArc(x+offset, y+offset, outerRadius, angle1, angle2)
// 绘制内圆弧(反向)
g.dc.DrawArc(x+offset, y+offset, innerRadius, angle2, angle1)
g.dc.ClosePath()
}, opt)
}
// Triangle 绘制三角形
func (g *Graphics) Triangle(x1, y1, x2, y2, x3, y3 float64, opt *DrawStyle) {
g.draw(func(offset float64) {
g.dc.MoveTo(x1+offset, y1+offset)
g.dc.LineTo(x2+offset, y2+offset)
g.dc.LineTo(x3+offset, y3+offset)
g.dc.ClosePath()
}, opt)
}
// Polygon 绘制多边形
func (g *Graphics) Polygon(points []float64, opt *DrawStyle) {
pp := convertPoints(points)
g.draw(func(offset float64) {
if len(pp) > 0 {
pp := convertPoints(points)
g.dc.MoveTo(pp[0].X+offset, pp[0].Y+offset)
for _, p := range pp[1:] {
g.dc.LineTo(p.X+offset, p.Y+offset)
}
g.dc.ClosePath()
}
}, opt)
}
// RegularPolygon 绘制正多边形
func (g *Graphics) RegularPolygon(n int, x, y, r float64, opt *DrawStyle) {
g.draw(func(offset float64) {
for i := 0; i < n; i++ {
angle := float64(i)*2*math.Pi/float64(n) - math.Pi/2
px := x + offset + r*math.Cos(angle)
py := y + offset + r*math.Sin(angle)
if i == 0 {
g.dc.MoveTo(px, py)
} else {
g.dc.LineTo(px, py)
}
}
g.dc.ClosePath()
}, opt)
}
// Star 绘制星形
func (g *Graphics) Star(n int, x, y, outerRadius, innerRadius float64, opt *DrawStyle) {
g.draw(func(offset float64) {
for i := 0; i < n*2; i++ {
angle := float64(i)*math.Pi/float64(n) - math.Pi/2
r := outerRadius
if i%2 == 1 {
r = innerRadius
}
px := x + offset + r*math.Cos(angle)
py := y + offset + r*math.Sin(angle)
if i == 0 {
g.dc.MoveTo(px, py)
} else {
g.dc.LineTo(px, py)
}
}
g.dc.ClosePath()
}, opt)
}
// BezierCurve 绘制贝塞尔曲线
func (g *Graphics) BezierCurve(points []float64, opt *DrawStyle) {
pp := convertPoints(points)
g.draw(func(offset float64) {
if len(pp) >= 4 && len(pp)%3 == 1 {
g.dc.MoveTo(pp[0].X+offset, pp[0].Y+offset)
for i := 1; i < len(pp); i += 3 {
g.dc.CubicTo(
pp[i].X+offset, pp[i].Y+offset,
pp[i+1].X+offset, pp[i+1].Y+offset,
pp[i+2].X+offset, pp[i+2].Y+offset,
)
}
}
}, opt)
}
// QuadraticCurve 绘制二次贝塞尔曲线
func (g *Graphics) QuadraticCurve(points []float64, opt *DrawStyle) {
pp := convertPoints(points)
g.draw(func(offset float64) {
if len(pp) >= 3 && len(pp)%2 == 1 {
g.dc.MoveTo(pp[0].X+offset, pp[0].Y+offset)
for i := 1; i < len(pp); i += 2 {
g.dc.QuadraticTo(
pp[i].X+offset, pp[i].Y+offset,
pp[i+1].X+offset, pp[i+1].Y+offset,
)
}
}
}, opt)
}
type PathCmd struct {
Cmd string
Args []string
}
type RelativePoint struct {
X, Y float64
}
func (p *RelativePoint) Parse(cmd string, arg string) gg.Point {
a := u.SplitWithoutNone(arg, ",")
if len(a) != 2 {
a = append(a, "")
}
if unicode.IsLower(rune(cmd[0])) {
p.X += u.Float64(a[0])
p.Y += u.Float64(a[1])
} else {
if a[0] != "" {
p.X = u.Float64(a[0])
}
if a[1] != "" {
p.Y = u.Float64(a[1])
}
}
return gg.Point{X: p.X, Y: p.Y}
}
// Path 绘制复杂路径(支持直线和曲线)
func (g *Graphics) Path(commands string, opt *DrawStyle) {
a := u.SplitWithoutNone(commands, " ")
cmds := make([]PathCmd, 0)
lastCmd := "M"
args := []string{}
for _, cmd := range a {
w1 := cmd[0]
if (w1 >= 'A' && w1 <= 'Z') || (w1 >= 'a' && w1 <= 'z') {
if len(args) > 0 {
// 遇到字母,先处理之前的命令
cmds = append(cmds, PathCmd{
Cmd: lastCmd,
Args: args,
})
args = []string{}
}
lastCmd = string(w1)
cmd = cmd[1:]
}
// 遇到结束命令
if lastCmd == "Z" || lastCmd == "z" {
cmds = append(cmds, PathCmd{Cmd: lastCmd})
break
}
// 无参数,继续等待
if len(cmd) > 0 {
args = append(args, cmd)
}
}
// 添加最后一个命令
if len(args) > 0 || lastCmd == "Z" || lastCmd == "z" {
cmds = append(cmds, PathCmd{Cmd: lastCmd, Args: args})
}
g.draw(func(offset float64) {
// 状态跟踪
p := RelativePoint{}
var startP *gg.Point
var prevControl gg.Point
for _, cmd := range cmds {
// 绝对/相对模式判断
switch cmd.Cmd {
case "M", "m":
if len(cmd.Args) > 0 {
pp := p.Parse(cmd.Cmd, cmd.Args[0])
startP = &pp
g.dc.MoveTo(pp.X+offset, pp.Y+offset)
// 如果后面还有参数则视为隐式的直线命令L或l
for i := 1; i < len(cmd.Args); i++ {
pp = p.Parse(cmd.Cmd, cmd.Args[i])
g.dc.LineTo(pp.X+offset, pp.Y+offset)
}
}
case "L", "l":
for _, arg := range cmd.Args {
pp := p.Parse(cmd.Cmd, arg)
g.dc.LineTo(pp.X+offset, pp.Y+offset)
}
case "C", "c":
for i := 0; i < len(cmd.Args)-2; i += 3 {
pp1 := p.Parse(cmd.Cmd, cmd.Args[i])
pp2 := p.Parse(cmd.Cmd, cmd.Args[i+1])
pp3 := p.Parse(cmd.Cmd, cmd.Args[i+2])
g.dc.CubicTo(
pp1.X+offset, pp1.Y+offset,
pp2.X+offset, pp2.Y+offset,
pp3.X+offset, pp3.Y+offset,
)
prevControl = pp2
}
case "S", "s":
// 简化三次贝塞尔曲线
for i := 0; i < len(cmd.Args); i += 2 {
if i+1 >= len(cmd.Args) {
break
}
// 计算第一个控制点(对称点)
cp1 := gg.Point{
X: 2*p.X - prevControl.X,
Y: 2*p.Y - prevControl.Y,
}
cp2 := p.Parse(cmd.Cmd, cmd.Args[i])
end := p.Parse(cmd.Cmd, cmd.Args[i+1])
g.dc.CubicTo(
cp1.X+offset, cp1.Y+offset,
cp2.X+offset, cp2.Y+offset,
end.X+offset, end.Y+offset,
)
prevControl = cp2
}
case "Q", "q":
// 二次贝塞尔曲线
for i := 0; i < len(cmd.Args); i += 2 {
if i+1 >= len(cmd.Args) {
break
}
cp := p.Parse(cmd.Cmd, cmd.Args[i])
end := p.Parse(cmd.Cmd, cmd.Args[i+1])
// 将二次转换为三次贝塞尔曲线
qToC1, qToC2 := quadToCubic(p.X, p.Y, cp.X, cp.Y, end.X, end.Y)
g.dc.CubicTo(
qToC1.X+offset, qToC1.Y+offset,
qToC2.X+offset, qToC2.Y+offset,
end.X+offset, end.Y+offset,
)
prevControl = cp
}
case "T", "t":
// 简化二次贝塞尔曲线
for _, arg := range cmd.Args {
// 计算控制点(对称点)
cp := gg.Point{
X: 2*p.X - prevControl.X,
Y: 2*p.Y - prevControl.Y,
}
end := p.Parse(cmd.Cmd, arg)
// 将二次转换为三次贝塞尔曲线
qToC1, qToC2 := quadToCubic(p.X, p.Y, cp.X, cp.Y, end.X, end.Y)
g.dc.CubicTo(
qToC1.X+offset, qToC1.Y+offset,
qToC2.X+offset, qToC2.Y+offset,
end.X+offset, end.Y+offset,
)
prevControl = cp
}
case "H", "h":
// 水平线
for _, arg := range cmd.Args {
pp := p.Parse(cmd.Cmd, arg+",")
g.dc.LineTo(pp.X+offset, pp.Y+offset)
}
case "V", "v":
// 垂直线
for _, arg := range cmd.Args {
pp := p.Parse(cmd.Cmd, ","+arg)
g.dc.LineTo(pp.X+offset, pp.Y+offset)
}
case "Z", "z":
if startP != nil {
g.dc.LineTo(startP.X+offset, startP.Y+offset)
}
g.dc.ClosePath()
}
}
}, opt)
}
// 辅助函数:二次到三次贝塞尔转换
func quadToCubic(x0, y0, cx, cy, x1, y1 float64) (gg.Point, gg.Point) {
return gg.Point{
X: x0 + (2.0/3.0)*(cx-x0),
Y: y0 + (2.0/3.0)*(cy-y0),
}, gg.Point{
X: x1 + (2.0/3.0)*(cx-x1),
Y: y1 + (2.0/3.0)*(cy-y1),
}
}
// Arrow 绘制箭头
func (g *Graphics) Arrow(x1, y1, x2, y2, headSize float64, opt *DrawStyle) {
g.draw(func(offset float64) {
// 计算箭头方向
dx := x2 - x1
dy := y2 - y1
angle := math.Atan2(dy, dx)
length := math.Sqrt(dx*dx + dy*dy)
// 绘制线
g.dc.MoveTo(x1+offset, y1+offset)
g.dc.LineTo(x2+offset, y2+offset)
// 绘制箭头头部
if headSize > 0 && length > headSize {
// 箭头头部点1
x3 := x2 - headSize*math.Cos(angle-math.Pi/6)
y3 := y2 - headSize*math.Sin(angle-math.Pi/6)
// 箭头头部点2
x4 := x2 - headSize*math.Cos(angle+math.Pi/6)
y4 := y2 - headSize*math.Sin(angle+math.Pi/6)
// 绘制箭头头部
g.dc.MoveTo(x2+offset, y2+offset)
g.dc.LineTo(x3+offset, y3+offset)
g.dc.MoveTo(x2+offset, y2+offset)
g.dc.LineTo(x4+offset, y4+offset)
}
}, opt)
}
// Grid 绘制网格
func (g *Graphics) Grid(x, y, width, height, step float64, opt *DrawStyle) {
g.draw(func(offset float64) {
// 水平线
for iy := y; iy <= y+height; iy += step {
g.dc.MoveTo(x+offset, iy+offset)
g.dc.LineTo(x+width+offset, iy+offset)
}
// 垂直线
for ix := x; ix <= x+width; ix += step {
g.dc.MoveTo(ix+offset, y+offset)
g.dc.LineTo(ix+offset, y+height+offset)
}
}, opt)
}
// Cross 绘制十字标记
func (g *Graphics) Cross(x, y, size float64, opt *DrawStyle) {
half := size / 2
g.draw(func(offset float64) {
// 水平线
g.dc.MoveTo(x-half+offset, y+offset)
g.dc.LineTo(x+half+offset, y+offset)
// 垂直线
g.dc.MoveTo(x+offset, y-half+offset)
g.dc.LineTo(x+offset, y+half+offset)
}, opt)
}
// RandBG 绘制随机干扰背景1-10档
// 档位说明:
//
// 1-3级: 轻微干扰,适合验证码文本前的背景
// 4-6级: 中等干扰,适合文本后的干扰层
// 7-10级: 强干扰有效防止OCR识别
func (g *Graphics) RandBG(level int) {
if level < 1 {
level = 1
} else if level > 10 {
level = 10
}
bounds := g.dc.Image().Bounds()
w, h := bounds.Dx(), bounds.Dy()
// 根据级别确定干扰元素数量 - 大幅增加密度
elements := 30 + level*150 // 30到1530个元素
// 绘制多种干扰元素 - 提高不透明度和尺寸
for i := 0; i < elements; i++ {
// 随机位置
x := rand.Float64() * float64(w)
y := rand.Float64() * float64(h)
// 大幅提高透明度 - 减少半透明效果
minAlpha := 120
maxAlpha := 230
if level >= 7 {
minAlpha = 150
maxAlpha = 255
}
r := uint8(rand.Intn(180))
gVal := uint8(rand.Intn(180))
bVal := uint8(rand.Intn(180))
a := uint8(minAlpha + rand.Intn(maxAlpha-minAlpha))
color := fmt.Sprintf("#%02X%02X%02X%02X", r, gVal, bVal, a)
// 提高元素尺寸上限
minSize := float64(1)
maxSize := 7.0 + float64(level)*1.5 // 最大尺寸从10提高到20
size := rand.Float64()*(maxSize-minSize) + minSize
// 线宽也要随级别增加
lineWidth := 0.5 + rand.Float64()*(0.5+float64(level)*0.3)
// 元素选择权重
elementType := rand.Intn(100)
// 增加高等级特殊干扰的概率
if level >= 7 && rand.Intn(100) < (level-6)*15 {
elementType = 95 + rand.Intn(5) // 特殊干扰类型
}
switch {
case elementType < 20: // 20%概率画点
g.Point(x, y, &color)
case elementType < 40: // 20%概率画短线
angle := rand.Float64() * 2 * math.Pi
length := 3 + rand.Float64()*float64(level)*3
x2 := x + math.Cos(angle)*length
y2 := y + math.Sin(angle)*length
g.Line(x, y, x2, y2, &DrawStyle{
LineColor: color,
LineWidth: lineWidth,
})
case elementType < 60: // 20%概率画小圆
radius := 0.5 + rand.Float64()*size
g.Circle(x, y, radius, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
})
case elementType < 80: // 20%概率画小矩形 - 增加比例
w := 1 + rand.Float64()*size*5
h := 1 + rand.Float64()*size*3
g.Rect(x, y, w, h, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
})
case elementType < 95: // 15%概率画微弧
startAngle := rand.Float64() * 2 * math.Pi
endAngle := startAngle + math.Pi/2 + rand.Float64()*math.Pi
g.Arc(x, y, size, startAngle, endAngle, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
})
default: // 5%概率画特殊干扰
// 随机选择一种高级干扰
switch rand.Intn(4) {
case 0: // 复杂多边形
points := make([]float64, 0, 14)
for j := 0; j < 7; j++ {
points = append(points, x+rand.Float64()*size*3, y+rand.Float64()*size*3)
}
g.Polygon(points, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
FillColor: color,
})
case 1: // 交叉网格
size := 2 + rand.Float64()*size
g.Line(x-size, y, x+size, y, &DrawStyle{LineColor: color, LineWidth: lineWidth})
g.Line(x, y-size, x, y+size, &DrawStyle{LineColor: color, LineWidth: lineWidth})
g.Line(x-size, y-size, x+size, y+size, &DrawStyle{LineColor: color, LineWidth: lineWidth})
g.Line(x+size, y-size, x-size, y+size, &DrawStyle{LineColor: color, LineWidth: lineWidth})
case 2: // 扇形环
inner := size * 0.3
outer := size
start := rand.Float64() * 2 * math.Pi
end := start + math.Pi/2 + rand.Float64()*math.Pi
g.ArcRing(x, y, inner, outer, start, end, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
FillColor: color,
})
case 3: // 星形
points := 4 + rand.Intn(5)
inner := size * 0.4
outer := size
g.Star(points, x, y, outer, inner, &DrawStyle{
LineWidth: lineWidth,
LineColor: color,
FillColor: color,
})
}
}
}
// 高级干扰特性
if level >= 7 {
// 增加高级干扰曲线的数量
g.drawCurveDisturbance(w, h, level)
// 增加噪点干扰
if level >= 8 {
g.addNoise(level, w, h)
}
}
}
// 绘制干扰曲线 - 修复参数错误
func (g *Graphics) drawCurveDisturbance(w, h, level int) {
// 大幅增加曲线数量
curves := 15 + (level-7)*20
for i := 0; i < curves; i++ {
// 使用更不透明的颜色
r := uint8(rand.Intn(150))
gVal := uint8(rand.Intn(150))
bVal := uint8(rand.Intn(150))
a := uint8(160 + rand.Intn(80))
color := fmt.Sprintf("#%02X%02X%02X%02X", r, gVal, bVal, a)
// 生成控制点 - 避免单个点问题
points := make([]float64, 0, 8)
for j := 0; j < 4; j++ {
points = append(points, rand.Float64()*float64(w))
points = append(points, rand.Float64()*float64(h))
}
// 绘制曲线 - 使用更宽的线条
g.BezierCurve(points, &DrawStyle{
LineColor: color,
LineWidth: 1.0 + rand.Float64()*3.0,
})
}
}
// 添加噪点干扰 - 修复并增强效果
func (g *Graphics) addNoise(level int, w, h int) {
// 创建噪点图层
noiseImg := image.NewRGBA(image.Rect(0, 0, w, h))
// 设置噪点密度 - 随等级增加
density := 0.002 + float64(level-8)*0.003
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if rand.Float64() < density {
// 随机颜色 - 高对比度
r := uint8(rand.Intn(200))
g := uint8(rand.Intn(200))
b := uint8(rand.Intn(200))
a := uint8(160 + rand.Intn(80))
noiseImg.SetRGBA(x, y, color.RGBA{r, g, b, a})
} else {
// 透明点
noiseImg.SetRGBA(x, y, color.RGBA{0, 0, 0, 0})
}
}
}
// 应用高斯模糊使噪点更自然
if level > 8 {
blurred := imaging.Blur(noiseImg, 0.8+float64(level-8)*0.4)
g.dc.DrawImage(blurred, 0, 0)
} else {
g.dc.DrawImage(noiseImg, 0, 0)
}
}

319
effect.go Normal file
View File

@ -0,0 +1,319 @@
package img
import (
"errors"
"image"
"image/color"
"image/draw"
"math"
"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/ssgo/u"
)
// Crop 裁剪图像
func (g *Graphics) Crop(x, y, w, h int) {
rect := image.Rect(x, y, x+w, y+h)
newImg := imaging.Crop(g.dc.Image(), rect)
g.dc = gg.NewContextForImage(newImg)
}
// Contain 改变图片尺寸,保持图片比例,图片居中
func (g *Graphics) Contain(width, height int) {
bounds := g.dc.Image().Bounds()
srcW := bounds.Dx()
srcH := bounds.Dy()
ratio := math.Min(float64(width)/float64(srcW), float64(height)/float64(srcH))
dstW := int(float64(srcW) * ratio)
dstH := int(float64(srcH) * ratio)
// 创建透明背景画布
newImg := image.NewRGBA(image.Rect(0, 0, width, height))
// 居中放置缩放后的图像
offsetX := (width - dstW) / 2
offsetY := (height - dstH) / 2
resized := imaging.Resize(g.dc.Image(), dstW, dstH, imaging.Lanczos)
draw.Draw(newImg,
image.Rect(offsetX, offsetY, offsetX+dstW, offsetY+dstH),
resized, image.Point{}, draw.Over)
g.dc = gg.NewContextForImage(newImg)
}
// Cover 改变图片尺寸,保持图片比例,图片居中裁剪
func (g *Graphics) Cover(width, height int) {
bounds := g.dc.Image().Bounds()
srcW := bounds.Dx()
srcH := bounds.Dy()
ratio := math.Max(float64(width)/float64(srcW), float64(height)/float64(srcH))
dstW := int(float64(srcW) * ratio)
dstH := int(float64(srcH) * ratio)
resized := imaging.Resize(g.dc.Image(), dstW, dstH, imaging.Lanczos)
// 从中心裁剪
cropX := max(0, (dstW-width)/2)
cropY := max(0, (dstH-height)/2)
cropped := imaging.Crop(resized,
image.Rect(cropX, cropY, cropX+width, cropY+height))
g.dc = gg.NewContextForImage(cropped)
}
// Resize 改变图片尺寸,拉伸
func (g *Graphics) Resize(width, height int, resampleMode *string) {
resample := imaging.Lanczos
if resampleMode != nil {
switch *resampleMode {
case "nearest":
resample = imaging.NearestNeighbor
case "catmullrom":
resample = imaging.CatmullRom
case "box":
resample = imaging.Box
case "linear":
resample = imaging.Linear
case "hermite":
resample = imaging.Hermite
case "mitchellnetravali":
resample = imaging.MitchellNetravali
case "bspline":
resample = imaging.BSpline
case "gaussian":
resample = imaging.Gaussian
case "bartlett":
resample = imaging.Bartlett
case "hann":
resample = imaging.Hann
case "hamming":
resample = imaging.Hamming
case "blackman":
resample = imaging.Blackman
case "welch":
resample = imaging.Welch
case "cosine":
resample = imaging.Cosine
default:
resample = imaging.Lanczos
}
}
resized := imaging.Resize(g.dc.Image(), width, height, resample)
g.dc = gg.NewContextForImage(resized)
}
// Rotate 旋转图像
func (g *Graphics) Rotate(angle float64) {
rotated := imaging.Rotate(g.dc.Image(), angle, color.Transparent)
g.dc = gg.NewContextForImage(rotated)
}
// Blur 模糊图像
func (g *Graphics) Blur(sigma float64) {
blurred := imaging.Blur(g.dc.Image(), sigma)
g.dc = gg.NewContextForImage(blurred)
}
// Sharpen 锐化图像
func (g *Graphics) Sharpen(sigma float64) {
sharpened := imaging.Sharpen(g.dc.Image(), sigma)
g.dc = gg.NewContextForImage(sharpened)
}
// Grayscale 转为灰度图
func (g *Graphics) Grayscale() {
gray := imaging.Grayscale(g.dc.Image())
g.dc = gg.NewContextForImage(gray)
}
// AdjustBrightness 调整亮度
func (g *Graphics) AdjustBrightness(percent float64) {
adjusted := imaging.AdjustBrightness(g.dc.Image(), percent)
g.dc = gg.NewContextForImage(adjusted)
}
// AdjustContrast 调整对比度
func (g *Graphics) AdjustContrast(percent float64) {
adjusted := imaging.AdjustContrast(g.dc.Image(), percent)
g.dc = gg.NewContextForImage(adjusted)
}
// GammaCorrection Gamma校正
func (g *Graphics) GammaCorrection(gamma float64) {
adjusted := imaging.AdjustGamma(g.dc.Image(), gamma)
g.dc = gg.NewContextForImage(adjusted)
}
// FlipH 水平翻转
func (g *Graphics) FlipH() {
flipped := imaging.FlipH(g.dc.Image())
g.dc = gg.NewContextForImage(flipped)
}
// FlipV 垂直翻转
func (g *Graphics) FlipV() {
flipped := imaging.FlipV(g.dc.Image())
g.dc = gg.NewContextForImage(flipped)
}
// AdjustSaturation 调整饱和度
func (g *Graphics) AdjustSaturation(percent float64) {
adjusted := imaging.AdjustSaturation(g.dc.Image(), percent)
g.dc = gg.NewContextForImage(adjusted)
}
// func min(a, b int) int {
// if a < b {
// return a
// }
// return b
// }
// func max(a, b int) int {
// if a > b {
// return a
// }
// return b
// }
// Convolve3x3 应用3x3卷积核
func (g *Graphics) Convolve3x3(kernel []float64) error {
if len(kernel) != 9 {
return errors.New("kernel " + u.String(len(kernel)) + " must be 3x3")
}
// 获取图像大小
bounds := g.dc.Image().Bounds()
width, height := bounds.Dx(), bounds.Dy()
// 创建新图像
result := image.NewRGBA(bounds)
// 卷积核权重和
var sum float64
for _, v := range kernel {
sum += v
}
// 如果权重和不是0则使用归一化因子
scale := 1.0
if sum != 0 {
scale = 1.0 / sum
}
// 处理每个像素
for y := 1; y < height-1; y++ {
for x := 1; x < width-1; x++ {
var r, g1, b, a float64
// 应用卷积核
for ky := -1; ky <= 1; ky++ {
for kx := -1; kx <= 1; kx++ {
k := kernel[(ky+1)*3+(kx+1)]
px := g.dc.Image().At(x+kx, y+ky)
pr, pg, pb, pa := px.RGBA()
r += float64(pr>>8) * k
g1 += float64(pg>>8) * k
b += float64(pb>>8) * k
a += float64(pa>>8) * k
}
}
// 应用比例因子并限制范围
r = math.Max(0, math.Min(255, r*scale))
g1 = math.Max(0, math.Min(255, g1*scale))
b = math.Max(0, math.Min(255, b*scale))
a = math.Max(0, math.Min(255, a*scale))
// 设置结果像素
result.SetRGBA(x, y, color.RGBA{
R: uint8(r),
G: uint8(g1),
B: uint8(b),
A: uint8(a),
})
}
}
// 复制边界像素
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if y == 0 || y == height-1 || x == 0 || x == width-1 {
result.Set(x, y, g.dc.Image().At(x, y))
}
}
}
g.dc = gg.NewContextForImage(result)
return nil
}
// 带强度参数的滤镜应用
func (g *Graphics) ApplyFilter(kernel []float64, strength *float64) error {
if len(kernel) != 9 {
return errors.New("kernel " + u.String(len(kernel)) + " must be 3x3")
}
// 创建加权核
weightedKernel := make([]float64, len(kernel))
if strength == nil {
copy(weightedKernel, kernel)
} else {
centerIdx := 4 // 3x3核心中心索引
for i, v := range kernel {
if i == centerIdx {
weightedKernel[i] = 1 + *strength*(v-1)
} else {
weightedKernel[i] = *strength * v
}
}
}
return g.Convolve3x3(weightedKernel)
}
// 边缘检测
func (g *Graphics) EdgeDetectionFilter(strength *float64) {
g.ApplyFilter([]float64{
-1, -1, -1,
-1, 8, -1,
-1, -1, -1,
}, strength)
}
// 高斯模糊
func (g *Graphics) GaussianBlurFilter(strength *float64) {
g.ApplyFilter([]float64{
1, 2, 1,
2, 4, 2,
1, 2, 1,
}, strength)
}
// 锐化
func (g *Graphics) SharpenFilter(strength *float64) {
g.ApplyFilter([]float64{
-1, -1, -1,
-1, 9, -1,
-1, -1, -1,
}, strength)
}
// 浮雕
func (g *Graphics) EmbossFilter(strength *float64) {
g.ApplyFilter([]float64{
-2, -1, 0,
-1, 1, 1,
0, 1, 2,
}, strength)
}
// 散景模糊
func (g *Graphics) BokehFilter(strength *float64) {
g.ApplyFilter([]float64{
0, 0.1, 0,
0.1, 0.6, 0.1,
0, 0.1, 0,
}, strength)
}

28
go.mod Normal file
View File

@ -0,0 +1,28 @@
module apigo.cc/gojs/img
go 1.24
require (
apigo.cc/gojs v0.0.25
apigo.cc/gojs/console v0.0.2
github.com/disintegration/imaging v1.6.2
github.com/flopp/go-findfont v0.1.0
github.com/fogleman/gg v1.3.0
github.com/ssgo/u v1.7.21
golang.org/x/image v0.29.0
)
require (
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
github.com/ssgo/config v1.7.9 // indirect
github.com/ssgo/log v1.7.9 // indirect
github.com/ssgo/standard v1.7.7 // indirect
github.com/ssgo/tool v0.4.29 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

332
img.go Normal file
View File

@ -0,0 +1,332 @@
package img
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"math"
"math/rand"
"os"
"strconv"
"strings"
"apigo.cc/gojs"
"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"golang.org/x/image/font"
)
type Graphics struct {
// img image.Image
bgColor string
lastColor string
lastFont font.Face
dc *gg.Context // 高级绘图上下文
}
func init() {
obj := map[string]any{
"createImage": CreateImage,
"loadImage": LoadImage,
"loadFont": LoadFont,
"listFont": ListFont,
"randColor": RandColor,
"lineCap": lineCap,
"lineJoin": lineJoin,
"fillRule": fillRule,
}
gojs.Register("apigo.cc/gojs/img", gojs.Module{
Object: gojs.ToMap(obj),
TsCode: gojs.MakeTSCode(obj),
})
}
// CreateImage 创建一个新的图像
func CreateImage(width, height int, c *string) *Graphics {
img := image.NewRGBA(image.Rect(0, 0, width, height))
dc := gg.NewContextForImage(img)
g := &Graphics{
// img: dc.Image(),
dc: dc,
}
if c != nil {
g.bgColor = *c
g.SetColor(*c)
g.dc.Clear()
}
return g
}
// LoadImage 从文件加载图像
func LoadImage(filePath string) (*Graphics, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return &Graphics{
// img: img,
dc: gg.NewContextForImage(img),
}, nil
}
func (g *Graphics) Clear(x, y, width, height int) {
if g.bgColor != "" {
g.dc.DrawRectangle(float64(x), float64(y), float64(width), float64(height))
g.dc.SetColor(hexToRGBA(g.bgColor))
g.dc.Fill()
return
}
if img, ok := g.dc.Image().(*image.RGBA); ok {
transparent := image.NewUniform(color.RGBA{0, 0, 0, 0})
draw.Draw(img, image.Rect(x, y, x+width, y+height), transparent, image.Point{}, draw.Src)
}
}
// SubImage 提取指定区域的子图
func (g *Graphics) Sub(x, y, width, height int) *Graphics {
// bounds := image.Rect(x, y, x+width, y+height)
newImg := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(newImg, newImg.Bounds(), g.dc.Image(), image.Pt(x, y), draw.Src)
newDC := gg.NewContextForImage(newImg)
return &Graphics{
// img: newDC.Image(),
dc: newDC,
bgColor: g.bgColor,
lastColor: g.lastColor,
lastFont: g.lastFont,
}
}
// Clone 创建图像的深拷贝
func (g *Graphics) Clone() *Graphics {
bounds := g.dc.Image().Bounds()
newImg := image.NewRGBA(bounds)
draw.Draw(newImg, bounds, g.dc.Image(), bounds.Min, draw.Src)
return &Graphics{
// img: newImg,
dc: gg.NewContextForImage(newImg),
}
}
func (g *Graphics) SetColor(c string) {
g.lastColor = c
g.dc.SetColor(hexToRGBA(c))
}
// // Image 绘制图片(支持多种布局模式)
// func (g *Graphics) Image(src *Graphics, x, y, width, height int, mode string) {
// if src == nil {
// return
// }
// srcBounds := src.img.Bounds()
// srcW := srcBounds.Dx()
// srcH := srcBounds.Dy()
// // 计算目标尺寸
// targetW := width
// targetH := height
// // 根据不同模式计算实际绘制区域
// var drawW, drawH int
// switch mode {
// case scaleMode.Fill:
// drawW = targetW
// drawH = targetH
// case scaleMode.Contain:
// ratio := math.Min(float64(targetW)/float64(srcW), float64(targetH)/float64(srcH))
// drawW = int(float64(srcW) * ratio)
// drawH = int(float64(srcH) * ratio)
// case scaleMode.Cover:
// ratio := math.Max(float64(targetW)/float64(srcW), float64(targetH)/float64(srcH))
// drawW = int(float64(srcW) * ratio)
// drawH = int(float64(srcH) * ratio)
// }
// // 计算居中位置
// offsetX := (targetW - drawW) / 2
// offsetY := (targetH - drawH) / 2
// // 缩放图片
// scaled := imaging.Resize(src.img, drawW, drawH, imaging.Lanczos)
// // 绘制图片
// g.dc.DrawImage(scaled, x+offsetX, y+offsetY)
// g.img = g.dc.Image()
// }
// Put 简单的把图原样贴上去
func (g *Graphics) Put(src *Graphics, x, y int) {
if src == nil || src.dc == nil {
return
}
// 直接绘制源图像,不进行缩放
g.dc.DrawImage(src.dc.Image(), x, y)
// g.img = g.dc.Image() // 更新主图像
}
// PutTo 将原图整张贴进去根据mode处理尺寸
func (g *Graphics) PutTo(src *Graphics, dx, dy, dw, dh int, sizeMode string) {
// 调用完整功能函数,源区域是整个源图像
srcBounds := src.dc.Image().Bounds()
g.PutBy(src, srcBounds.Min.X, srcBounds.Min.Y, srcBounds.Dx(), srcBounds.Dy(), dx, dy, dw, dh, sizeMode)
}
// PutBy 完整的贴图功能
func (g *Graphics) PutBy(src *Graphics, sx, sy, sw, sh, dx, dy, dw, dh int, sizeMode string) {
// 基础检查
if src == nil || src.dc == nil || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 {
return
}
// 1. 裁剪源图像到指定区域
srcBounds := src.dc.Image().Bounds()
cropX := max(sx, srcBounds.Min.X)
cropY := max(sy, srcBounds.Min.Y)
cropW := min(sw, srcBounds.Dx())
cropH := min(sh, srcBounds.Dy())
cropped := imaging.Crop(src.dc.Image(), image.Rect(cropX, cropY, cropX+cropW, cropY+cropH))
// 2. 根据模式计算缩放尺寸
var (
drawW, drawH int
ratio float64
)
switch sizeMode {
case "contain":
ratio = math.Min(float64(dw)/float64(cropW), float64(dh)/float64(cropH))
drawW = int(float64(cropW) * ratio)
drawH = int(float64(cropH) * ratio)
case "cover":
ratio = math.Max(float64(dw)/float64(cropW), float64(dh)/float64(cropH))
drawW = int(float64(cropW) * ratio)
drawH = int(float64(cropH) * ratio)
case "fill":
drawW = dw
drawH = dh
}
// 3. 居中偏移计算
offsetX := (dw - drawW) / 2
offsetY := (dh - drawH) / 2
// 4. 缩放图像
scaled := imaging.Resize(cropped, drawW, drawH, imaging.Lanczos)
// 5. 绘制到目标位置
g.dc.DrawImage(scaled, dx+offsetX, dy+offsetY)
// g.img = g.dc.Image()
}
// FillAreaWithImage 用图像填充目标区域
func (g *Graphics) FillAreaWithImage(src *Graphics, x, y, width, height int) {
if src == nil {
return
}
srcBounds := src.dc.Image().Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
// 计算需要复制的次数
cols := (width + srcW - 1) / srcW
rows := (height + srcH - 1) / srcH
for row := 0; row < rows; row++ {
for col := 0; col < cols; col++ {
px := x + col*srcW
py := y + row*srcH
g.dc.DrawImage(src.dc.Image(), px, py)
}
}
// g.img = g.dc.Image()
}
// ExportImage 导出图像到文件
func (g *Graphics) ExportImage(filePath string, quality *int) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
switch {
case strings.HasSuffix(strings.ToLower(filePath), ".png"):
return png.Encode(file, g.dc.Image())
case strings.HasSuffix(strings.ToLower(filePath), ".jpg"),
strings.HasSuffix(strings.ToLower(filePath), ".jpeg"):
q := 85 // 默认质量
if quality != nil {
q = *quality
if q < 0 {
q = 0
} else if q > 100 {
q = 100
}
}
return jpeg.Encode(file, g.dc.Image(), &jpeg.Options{Quality: q})
default:
return fmt.Errorf("unsupported file format: %s", filePath)
}
}
// GetImageData 获取图像原始数据
// func (g *Graphics) GetImageData() image.Image {
// return g.img
// }
// hexToRGBA 将多种格式的十六进制颜色字符串转换为 color.RGBA
// 支持格式: #RRGGBB, #RRGGBBAA, #RGB, #RGBA
func hexToRGBA(hex string) color.Color {
// 去除开头的 # 号并转为大写
hex = strings.ToUpper(strings.TrimPrefix(hex, "#"))
// 验证合法字符
for _, ch := range hex {
if !((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) {
return color.RGBA{}
}
}
switch len(hex) {
case 3:
hex = fmt.Sprintf("%c%c%c%c%c%c", hex[0], hex[0], hex[1], hex[1], hex[2], hex[2])
case 4:
hex = fmt.Sprintf("%c%c%c%c%c%c%c%c", hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3])
}
switch len(hex) {
case 6: // #RRGGBB
return color.RGBA{R: parseHex(hex[0:2]), G: parseHex(hex[2:4]), B: parseHex(hex[4:6]), A: 255}
case 8: // #RRGGBBAA
return color.RGBA{R: parseHex(hex[0:2]), G: parseHex(hex[2:4]), B: parseHex(hex[4:6]), A: parseHex(hex[6:8])}
}
return color.RGBA{}
}
func parseHex(s string) uint8 {
val, _ := strconv.ParseUint(s, 16, 8)
return uint8(val)
}
// 辅助函数:生成对比色
func RandColor() string {
r := uint8(rand.Intn(150) + 50)
gr := uint8(rand.Intn(150) + 50)
b := uint8(rand.Intn(150) + 50)
a := uint8(rand.Intn(150) + 105)
return fmt.Sprintf("#%02X%02X%02X%02X", r, gr, b, a)
}

27
img_test.go Normal file
View File

@ -0,0 +1,27 @@
package img_test
import (
"os"
"testing"
"apigo.cc/gojs"
_ "apigo.cc/gojs/console"
_ "apigo.cc/gojs/img"
)
func TestCreateImage(t *testing.T) {
defer func() {
// os.Remove("testimg")
}()
gojs.ExportForDev()
os.Mkdir("testimg", 0755)
_, err := gojs.RunFile("img_test.js")
if err != nil {
t.Fatal(err)
}
// runtime.GC()
// time.Sleep(time.Second)
// if r != true {
// t.Fatal("run file failed, ", r)
// }
}

461
img_test.js Normal file
View File

@ -0,0 +1,461 @@
import img from "apigo.cc/gojs/img"
import co from "apigo.cc/gojs/console"
let g = img.createImage(1400, 2400)
// let g = img.createImage(300, 300)
g.setSansSerifFont(16)
// 创建区块网格
const b = []
for (let i = 0; i < 40; i++) {
const col = i % 5
const row = Math.floor(i / 5)
const x = 25 + col * (250 + 25)
const y = 25 + row * (250 + 25)
g.rect(x, y, 250, 250, { lineColor: "#666", lineWidth: 2 })
b.push({ x, y, width: 250, height: 250, idx: i })
}
function title(i, text) {
g.text(b[i].x, b[i].y + 250 - 30, text, { color: "#fff", bgColor: '#666', width: 250, height: 30, align: 'center', vAlign: 'middle' })
}
i = 0
title(i, "点")
for (x = 10; x < 250; x += 10) {
for (y = 10; y < 220; y += 10) {
g.point(b[i].x + x, b[i].y + y, img.randColor())
}
}
i++
title(i, "线")
g.line(b[i].x + 10, b[i].y + 10, b[i].x + 240, b[i].y + 210, { lineColor: img.randColor(), lineWidth: 1 })
g.line(b[i].x + 10, b[i].y + 50, b[i].x + 240, b[i].y + 50, { lineColor: img.randColor(), lineWidth: 3 })
g.line(b[i].x + 10, b[i].y + 115, b[i].x + 240, b[i].y + 115, { lineColor: img.randColor(), lineWidth: 5 })
i++
title(i, "矩形")
g.rect(b[i].x + 15, b[i].y + 10, 100, 50, { lineColor: img.randColor(), lineWidth: 1 })
g.rect(b[i].x + 15, b[i].y + 80, 100, 50, { lineColor: '#f90', lineWidth: 3, shadowOffset: 2 })
g.roundedRect(b[i].x + 15, b[i].y + 150, 100, 50, 10, { lineColor: '#f63', lineWidth: 5, shadowColor: '#999', shadowOffset: 3 })
g.rect(b[i].x + 135, b[i].y + 10, 100, 50, { fillColor: img.randColor(), lineWidth: 1 })
g.rect(b[i].x + 135, b[i].y + 80, 100, 50, { lineColor: '#f90', fillColor: img.randColor(), lineWidth: 3, shadowOffset: 2, shadowBlur: 3 })
g.roundedRect(b[i].x + 135, b[i].y + 150, 100, 50, 10, { lineColor: '#f63', fillColor: img.randColor(), lineWidth: 5, shadowColor: '#999', shadowOffset: 4, shadowBlur: 3 })
i++
title(i, "圆形和椭圆")
g.circle(b[i].x + 70, b[i].y + 70, 50, { lineColor: '#f00', lineWidth: 2 })
g.circle(b[i].x + 180, b[i].y + 70, 50, { fillColor: '#ff0', lineWidth: 3 })
g.ellipse(b[i].x + 70, b[i].y + 170, 60, 30, { lineColor: '#0f0', lineWidth: 2 })
g.ellipse(b[i].x + 180, b[i].y + 150, 40, 60, { fillColor: '#0ff', lineWidth: 1, shadowOffset: 2 })
i++
title(i, "多边形")
g.triangle(b[i].x + 10, b[i].y + 10, b[i].x + 10, b[i].y + 150, b[i].x + 120, b[i].y + 10)
g.polygon([b[i].x + 125, b[i].y + 50, b[i].x + 50, b[i].y + 150, b[i].x + 200, b[i].y + 150], {
lineColor: '#00f',
lineWidth: 3,
fillColor: '#ccf'
})
g.regularPolygon(6, b[i].x + 125, b[i].y + 170, 40, {
lineColor: '#f0f',
lineWidth: 2,
fillColor: '#fcf',
shadowOffset: 2
})
i++
title(i, "弧和扇形")
g.arc(b[i].x + 70, b[i].y + 70, 50, 0, Math.PI * 1.5, {
lineColor: '#f00',
lineWidth: 3
})
g.sector(b[i].x + 190, b[i].y + 120, 50, Math.PI / 4, Math.PI * 1.5, {
lineColor: '#0f0',
lineWidth: 2,
fillColor: '#cfc',
shadowOffset: 2,
})
i++
title(i, "圆环和弧环")
g.ring(b[i].x + 70, b[i].y + 70, 30, 50, {
lineColor: '#00f',
lineWidth: 3,
})
g.arcRing(b[i].x + 190, b[i].y + 120, 20, 50, Math.PI / 4, Math.PI * 1.5, {
lineColor: '#f0f',
lineWidth: 2,
fillColor: '#fcf',
shadowColor: '#0ff',
shadowOffset: -4,
})
i++
title(i, "箭头和十字")
g.arrow(b[i].x + 50, b[i].y + 70, b[i].x + 200, b[i].y + 70, 15, {
lineColor: '#f00',
lineWidth: 3
})
g.arrow(b[i].x + 200, b[i].y + 150, b[i].x + 50, b[i].y + 150, 20, {
lineColor: '#00f',
lineWidth: 4,
shadowOffset: 2,
})
g.cross(b[i].x + 125, b[i].y + 110, 40, {
lineColor: '#0a0',
lineWidth: 10,
shadowOffset: 4,
})
i++
title(i, "曲线")
g.bezierCurve([
b[i].x + 20, b[i].y + 150,
b[i].x + 100, b[i].y + 50,
b[i].x + 150, b[i].y + 200,
b[i].x + 230, b[i].y + 100
], {
lineColor: '#f00',
lineWidth: 2
})
g.quadraticCurve([
b[i].x + 20, b[i].y + 100,
b[i].x + 125, b[i].y + 200,
b[i].x + 230, b[i].y + 100
], {
lineColor: '#00f',
lineWidth: 2
})
i++
title(i, "虚线样式")
g.line(b[i].x + 20, b[i].y + 50, b[i].x + 230, b[i].y + 50, {
lineColor: '#f00',
lineWidth: 3,
dash: [10, 5]
})
g.line(b[i].x + 20, b[i].y + 100, b[i].x + 230, b[i].y + 100, {
lineColor: '#0f0',
lineWidth: 3,
dash: [5, 5]
})
g.line(b[i].x + 20, b[i].y + 150, b[i].x + 230, b[i].y + 150, {
lineColor: '#00f',
lineWidth: 3,
dash: [15, 5, 5, 5]
})
i++
title(i, "线帽样式")
g.line(b[i].x + 50, b[i].y + 50, b[i].x + 200, b[i].y + 50, {
lineColor: '#f00',
lineWidth: 10,
lineCap: img.lineCap.butt
})
g.line(b[i].x + 50, b[i].y + 100, b[i].x + 200, b[i].y + 100, {
lineColor: '#0f0',
lineWidth: 10,
lineCap: img.lineCap.round
})
g.line(b[i].x + 50, b[i].y + 150, b[i].x + 200, b[i].y + 150, {
lineColor: '#00f',
lineWidth: 10,
lineCap: img.lineCap.square
})
i++
title(i, "线连接样式")
g.lines([b[i].x + 50, b[i].y + 50, b[i].x + 150, b[i].y + 100, b[i].x + 50, b[i].y + 150], {
lineColor: '#f00',
lineWidth: 8,
lineJoin: img.lineJoin.bevel
})
g.lines([b[i].x + 150, b[i].y + 50, b[i].x + 50, b[i].y + 100, b[i].x + 150, b[i].y + 150], {
lineColor: '#0f0',
lineWidth: 8,
lineJoin: img.lineJoin.miter
})
g.lines([b[i].x + 100, b[i].y + 50, b[i].x + 200, b[i].y + 100, b[i].x + 100, b[i].y + 150], {
lineColor: '#00f',
lineWidth: 8,
lineJoin: img.lineJoin.round
})
i++
title(i, "星形")
g.star(5, b[i].x + 70, b[i].y + 70, 50, 20, {
lineColor: '#f00',
lineWidth: 2,
})
g.star(6, b[i].x + 180, b[i].y + 70, 50, 25, {
lineColor: '#0f0',
lineWidth: 2,
fillColor: '#cfc'
})
g.star(8, b[i].x + 125, b[i].y + 160, 50, 20, {
lineColor: '#00f',
lineWidth: 2,
fillColor: '#ccf'
})
i++
title(i, "网格")
g.grid(b[i].x + 25, b[i].y + 10, 200, 200, 20, {
lineColor: '#ccc',
lineWidth: 1
})
i++
title(i, "文本")
g.setSerifFont(12)
g.text(b[i].x + 10, b[i].y + 10, "Hello", { color: "#f00" })
g.setSansSerifFont(16)
g.text(b[i].x + 10, b[i].y + 30, "你好呀", { color: "#f00", borderColor: '#f90', paddingX: 4 })
g.setSansSerifFont(20)
g.text(b[i].x + 10, b[i].y + 60, "你好呀", { color: "#f00", width: 100, height: 50, align: 'center', vAlign: 'middle', bgColor: '#eee', shadowOffset: 2, shadowColor: '#999', borderRadius: 4 })
g.setSansSerifFont(16)
g.text(b[i].x + 10, b[i].y + 120, "你好呀", { color: "#f00", width: 100, height: 50, align: 'right', vAlign: 'bottom', borderColor: '#f90', paddingX: 4, bgColor: '#ff0', borderRadius: 8 })
g.text(b[i].x + 10, b[i].y + 180, "截断测试", { color: "#f00", width: 50, noWrap: true, borderColor: '#f90', paddingX: 4, shadowOffset: 2, borderRadius: 4 })
g.text(b[i].x + 120, b[i].y + 10, "这是一段长文本Cut me cut me cut me\nabcdefg1234567890\n\r【结束】", {
color: "#00f",
width: 120,
align: 'center',
bgColor: '#cff',
borderWidth: 2,
borderRadius: 4,
paddingX: 8,
paddingY: 4,
lineHeight: 1.2,
})
i++
title(i, "一幅山水画")
const s = img.createImage(250, 220)
let s1x = b[i].x
let s1y = b[i].y
s.circle(200, 40, 20, { fillColor: '#FFD700', shadowColor: '#ff6', shadowBlur: 5 })
s.path('M-10,210 L70,120 L150,210 Z', { fillColor: '#7CA982' })
s.path('M130,210 L210,110 L270,210 Z', { fillColor: '#5A9E6F' })
s.path('M50,210 L120,90 L180,210 Z', { fillColor: '#4A7C59' })
s.path('M150,210 L220,130 L290,210 Z', { fillColor: '#3D6B4F' })
s.path('M0,200 C40,180 80,220 120,200 C160,180 200,220 240,200 L260,220 L0,220 Z', { fillColor: '#4A90E2' })
s.path('M0,210 C30,195 60,210 90,205 C120,200 150,215 180,210 C210,205 240,215 250,210 L250,220 L0,220 Z', { fillColor: '#3A7BC8' })
g.put(s, b[i].x, b[i].y)
i++
let s1
title(i, "缩放")
s1 = g.sub(s1x, s1y, 250, 220)
s1.resize(200, 200)
g.rect(b[i].x + 25, b[i].y + 10, 200, 200, { lineColor: '#f90' })
g.put(s1, b[i].x + 25, b[i].y + 10)
i++
title(i, "等比缩放")
s1 = g.sub(s1x, s1y, 250, 220)
s1.contain(200, 200)
g.rect(b[i].x + 25, b[i].y + 10, 200, 200, { lineColor: '#f90' })
g.put(s1, b[i].x + 25, b[i].y + 10)
i++
title(i, "裁剪缩放")
s1 = g.sub(s1x, s1y, 250, 220)
s1.cover(200, 200)
g.rect(b[i].x + 25, b[i].y + 10, 200, 200, { lineColor: '#f90' })
g.put(s1, b[i].x + 25, b[i].y + 10)
i++
// 裁剪操作
title(i, "图像裁剪")
s1 = s.clone()
s1.crop(0, 0, 200, 200) // 裁剪区域
g.rect(b[i].x + 25, b[i].y + 10, 200, 200, { lineColor: '#f90' })
g.put(s1, b[i].x + 25, b[i].y + 10)
i++
title(i, "旋转")
s1 = g.sub(s1x, s1y, 250, 220)
s1.resize(125, 110)
s1.rotate(45)
g.put(s1, b[i].x, b[i].y)
s1 = g.sub(s1x, s1y, 250, 220)
s1.resize(125, 110)
s1.rotate(-45)
g.put(s1, b[i].x + 80, b[i].y + 80)
i++
title(i, "水平翻转")
s1 = s.clone()
s1.flipH()
g.put(s1, b[i].x, b[i].y)
i++
title(i, "垂直翻转")
s1 = s.clone()
s1.flipV()
g.put(s1, b[i].x, b[i].y)
i++
// 灰度滤镜
title(i, "灰度滤镜")
s1 = s.clone()
s1.grayscale()
g.put(s1, b[i].x, b[i].y)
i++
// 高斯模糊
title(i, "高斯模糊")
s1 = s.clone()
s1.gaussianBlurFilter(2) // 模糊强度参数
g.put(s1, b[i].x, b[i].y)
i++
// 锐化滤镜
title(i, "锐化滤镜")
s1 = s.clone()
s1.sharpenFilter(0.5) // 锐化强度参数
g.put(s1, b[i].x, b[i].y)
i++
// 边缘检测
title(i, "边缘检测")
s1 = s.clone()
s1.edgeDetectionFilter(1) // 边缘检测强度
g.put(s1, b[i].x, b[i].y)
i++
// 浮雕效果
title(i, "浮雕效果")
s1 = s.clone()
s1.embossFilter(1.8) // 浮雕强度
g.put(s1, b[i].x, b[i].y)
i++
// 亮度调整
title(i, "亮度调整")
s1 = s.clone()
s1.adjustBrightness(30) // 增加30%亮度
g.put(s1, b[i].x, b[i].y)
i++
// 对比度调整
title(i, "对比度调整")
s1 = s.clone()
s1.adjustContrast(50) // 增加50%对比度
g.put(s1, b[i].x, b[i].y)
i++
// 饱和度调整
title(i, "饱和度调整")
s1 = s.clone()
s1.adjustSaturation(70) // 增加70%饱和度
g.put(s1, b[i].x, b[i].y)
i++
// 伽马校正
title(i, "伽马校正")
s1 = s.clone()
s1.gammaCorrection(1.8) // 伽马值
g.put(s1, b[i].x, b[i].y)
i++
title(i, "随机背景1-3")
s1 = img.createImage(250, 70)
s1.randBG(1)
g.put(s1, b[i].x, b[i].y)
g.rect(b[i].x, b[i].y, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(2)
g.put(s1, b[i].x, b[i].y + 75)
g.rect(b[i].x, b[i].y + 75, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(3)
g.put(s1, b[i].x, b[i].y + 150)
g.rect(b[i].x, b[i].y + 150, 250, 70, { lineColor: '#f90' })
i++
title(i, "随机背景4-6")
s1 = img.createImage(250, 70)
s1.randBG(4)
g.put(s1, b[i].x, b[i].y)
g.rect(b[i].x, b[i].y, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(5)
g.put(s1, b[i].x, b[i].y + 75)
g.rect(b[i].x, b[i].y + 75, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(6)
g.put(s1, b[i].x, b[i].y + 150)
g.rect(b[i].x, b[i].y + 150, 250, 70, { lineColor: '#f90' })
i++
title(i, "随机背景7-9")
s1 = img.createImage(250, 70)
s1.randBG(7)
g.put(s1, b[i].x, b[i].y)
g.rect(b[i].x, b[i].y, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(8)
g.put(s1, b[i].x, b[i].y + 75)
g.rect(b[i].x, b[i].y + 75, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.randBG(9)
g.put(s1, b[i].x, b[i].y + 150)
g.rect(b[i].x, b[i].y + 150, 250, 70, { lineColor: '#f90' })
i++
title(i, "图片验证码")
s1 = img.createImage(250, 70)
s1.setSansSerifFont(24)
s1.randBG(2)
s1.randText("123456")
s1.randBG(1)
g.put(s1, b[i].x, b[i].y)
g.rect(b[i].x, b[i].y, 250, 70, { lineColor: '#f90' })
for (let j = 1; j <= 5; j++) {
let lvB = j
let lvT = j <= 3 ? 1 : 2
title(i, "验证码 背景Lv " + lvB + '+' + lvT)
s1 = img.createImage(250, 70)
s1.setSansSerifFont(24)
s1.randBG(lvB)
s1.randText("123456")
s1.randBG(lvT)
g.put(s1, b[i].x, b[i].y)
g.rect(b[i].x, b[i].y, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.setSansSerifFont(24)
s1.randBG(lvB)
let r = s1.randText("你拧情")
s1.randBG(lvT)
for (let rect of r) {
s1.rect(rect[0], rect[1], rect[2], rect[3], { lineColor: '#f90' })
}
g.put(s1, b[i].x, b[i].y + 75)
g.rect(b[i].x, b[i].y + 75, 250, 70, { lineColor: '#f90' })
s1 = img.createImage(250, 70)
s1.setSansSerifFont(24)
s1.randBG(lvB)
s1.randText("A1CL")
s1.randBG(lvT)
g.put(s1, b[i].x, b[i].y + 150)
g.rect(b[i].x, b[i].y + 150, 250, 70, { lineColor: '#f90' })
i++
}
// // ========================= 导出图像 =========================
g.exportImage("testimg/全面绘图测试.png", 95)
co.log("测试图像生成完成!")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

731
text.go Normal file
View File

@ -0,0 +1,731 @@
package img
import (
"image"
"image/color"
"math"
"math/rand"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/flopp/go-findfont"
"github.com/ssgo/u"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
)
// 各操作系统默认字体文件列表
var defaultFontFiles = map[string]map[string][]string{
"windows": {
"serif": {"simsun.ttc", "times.ttf"},
"sans-serif": {"msyh.ttc", "arial.ttf"},
"monospace": {"consola.ttf", "simsun.ttc"},
},
"darwin": {
"serif": {"Songti.ttc", "Times New Roman.ttf"},
"sans-serif": {"Hiragino Sans GB.ttc", "PingFang.ttc", "Helvetica.ttf"},
"monospace": {"Menlo.ttc", "Courier New.ttf", "Hiragino Sans GB.ttc"},
},
"linux": {
"serif": {"dejavu/DejaVuSerif.ttf", "wqy-microhei.ttc", "noto/NotoSerifCJK-Regular.ttc"},
"sans-serif": {"dejavu/DejaVuSans.ttf", "wqy-microhei.ttc", "noto/NotoSansCJK-Regular.ttc"},
"monospace": {"dejavu/DejaVuSansMono.ttf", "wqy-microhei_mono.ttc", "droid/DroidSansMono.ttf"},
},
}
var defaultFontNames = map[string]map[string][]string{
"windows": {
"serif": {"SimSun", "Times New Roman"},
"sans-serif": {"Microsoft YaHei", "Arial"},
"monospace": {"Consolas", "SimSun"},
},
"darwin": {
"serif": {"Songti SC", "Times New Roman"},
"sans-serif": {"Hiragino Sans GB", "PingFang SC", "Helvetica"},
"monospace": {"Menlo", "Courier New", "Hiragino Sans GB"},
},
"linux": {
"serif": {"DejaVu Serif", "WenQuanYi Micro Hei", "Noto Serif CJK SC"},
"sans-serif": {"DejaVu Sans", "WenQuanYi Micro Hei", "Noto Sans CJK SC"},
"monospace": {"DejaVu Sans Mono", "WenQuanYi Micro Hei Mono", "Droid Sans Mono"},
},
}
var defaultFontLoaded = map[string]bool{}
var fontLoaded = map[string]bool{}
var fontCache = map[string]*sfnt.Font{}
var fontLock = sync.RWMutex{}
type CompositeFace struct {
Names []string
Faces []font.Face
}
// 获取字体 - 支持TTF, OTF, TTC格式
func saveFontCache(buf *sfnt.Buffer, f *sfnt.Font) {
fullName, _ := f.Name(buf, sfnt.NameIDFull)
enFamily, _ := f.Name(buf, sfnt.NameIDFamily)
enStyle, _ := f.Name(buf, sfnt.NameIDSubfamily)
cnFamily, _ := f.Name(buf, sfnt.NameIDTypographicFamily)
cnStyle, _ := f.Name(buf, sfnt.NameIDTypographicSubfamily)
fullName = strings.TrimSpace(fullName)
enFamily = strings.TrimSpace(enFamily)
cnFamily = strings.TrimSpace(cnFamily)
enStyle = strings.TrimSpace(enStyle)
cnStyle = strings.TrimSpace(cnStyle)
enName := enFamily + " " + enStyle
cnName := cnFamily + " " + cnStyle
enName = strings.TrimSpace(enName)
cnName = strings.TrimSpace(cnName)
fontLock.Lock()
if enName != "" && fontCache[enName] == nil {
fontCache[enName] = f
}
if cnName != "" && fontCache[cnName] == nil {
fontCache[cnName] = f
}
if fullName != "" && fullName != enName && fontCache[fullName] == nil {
fontCache[fullName] = f
}
if enFamily != "" && fontCache[enFamily] == nil {
fontCache[enFamily] = f
}
if cnFamily != "" && fontCache[cnFamily] == nil {
fontCache[cnFamily] = f
}
fontLock.Unlock()
}
func ListFont() []string {
fonts := []string{}
fontLock.RLock()
defer fontLock.RUnlock()
for k := range fontCache {
fonts = append(fonts, k)
}
return fonts
}
var hasFontLoaded = false
func LoadFont(fontFiles ...string) {
fontLock.Lock()
if !hasFontLoaded {
hasFontLoaded = true
}
fontLock.Unlock()
if len(fontFiles) == 0 {
// fontFiles = findfont.List()
if osFFs, ok := defaultFontFiles[runtime.GOOS]; ok {
if ffs, ok := osFFs["serif"]; ok {
fontFiles = append(fontFiles, ffs...)
}
if ffs, ok := osFFs["sans-serif"]; ok {
fontFiles = append(fontFiles, ffs...)
}
if ffs, ok := osFFs["monospace"]; ok {
fontFiles = append(fontFiles, ffs...)
}
}
}
buf := &sfnt.Buffer{}
for _, fontFile := range fontFiles {
fontLock.RLock()
loaded := fontLoaded[fontFile]
fontLock.RUnlock()
if loaded {
continue
}
if !filepath.IsAbs(fontFile) {
if f, err := findfont.Find(fontFile); err == nil {
fontFile = f
}
}
if fontData, err := u.ReadFileBytes(fontFile); err == nil {
if strings.HasSuffix(fontFile, ".ttc") {
if c, err := sfnt.ParseCollection(fontData); err == nil {
for i := c.NumFonts() - 1; i >= 0; i-- {
if f, err := c.Font(i); err == nil {
saveFontCache(buf, f)
}
}
}
} else {
if f, err := sfnt.Parse(fontData); err == nil {
saveFontCache(buf, f)
}
}
}
fontLock.Lock()
fontLoaded[fontFile] = true
fontLock.Unlock()
}
}
// 获取字体 - 支持TTF, OTF, TTC格式
func makeFont(size float64, fontNames ...string) *CompositeFace {
fontLock.RLock()
hasFontLoaded1 := hasFontLoaded
fontLock.RUnlock()
if !hasFontLoaded1 {
LoadFont()
}
cf := &CompositeFace{Faces: []font.Face{}, Names: fontNames}
for _, name := range fontNames {
fontLock.RLock()
f := fontCache[name]
fontLock.RUnlock()
if f != nil {
if fc, err := opentype.NewFace(f, &opentype.FaceOptions{Size: size, DPI: 72}); err == nil {
cf.Faces = append(cf.Faces, fc)
// fmt.Println(u.BGreen("font"), name)
// return fc
}
}
}
return cf
// // fmt.Println(u.BRed("no font"))
// return nil
}
func loadDefaultFonts(typ string) {
fontLock.RLock()
loaded := defaultFontLoaded[runtime.GOOS+typ]
fontLock.RUnlock()
if loaded {
return
}
if osFFs, ok := defaultFontFiles[runtime.GOOS]; ok {
if ffs, ok := osFFs[typ]; ok {
LoadFont(ffs...)
}
}
fontLock.Lock()
defaultFontLoaded[runtime.GOOS+typ] = true
fontLock.Unlock()
}
func getDefaultFonts(typ string) []string {
if osFNs, ok := defaultFontNames[runtime.GOOS]; ok {
if fns, ok := osFNs[typ]; ok {
return fns
}
}
return []string{}
}
// 设置预设字体
func (g *Graphics) SetSerifFont(size float64) {
loadDefaultFonts("serif")
g.SetFont(size, getDefaultFonts("serif")...)
}
func (g *Graphics) SetSansSerifFont(size float64) {
loadDefaultFonts("sans-serif")
g.SetFont(size, getDefaultFonts("sans-serif")...)
}
func (g *Graphics) SetMonospaceFont(size float64) {
loadDefaultFonts("monospace")
g.SetFont(size, getDefaultFonts("monospace")...)
}
func (g *Graphics) SetFont(size float64, fontNames ...string) {
if len(fontNames) == 0 && g.lastFont != nil {
if ff, ok := g.lastFont.(*CompositeFace); ok {
fontNames = ff.Names
}
}
g.lastFont = makeFont(size, fontNames...)
g.dc.SetFontFace(g.lastFont)
}
func (c *CompositeFace) Glyph(dot fixed.Point26_6, r rune) (
dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
// fmt.Println(u.BCyan("Glyph"), len(c.Faces))
for _, face := range c.Faces {
dr, mask, maskp, advance, ok = face.Glyph(dot, r)
// fmt.Println(u.BCyan("Glyph >>> "), i, ok)
if ok {
return
}
}
return
}
func (c *CompositeFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
for _, face := range c.Faces {
bounds, advance, ok = face.GlyphBounds(r)
if ok {
return
}
}
return
}
func (c *CompositeFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
for _, face := range c.Faces {
advance, ok := face.GlyphAdvance(r)
if ok {
// fmt.Printf("字符 '%c' 使用字体%d 宽度=%d\n", r, i, advance)
return advance, true
}
}
// 找不到支持的字体
return fixed.I(1000), true // 返回默认宽度防止错误
}
func (c *CompositeFace) Kern(r0, r1 rune) fixed.Int26_6 {
for _, face := range c.Faces {
if kern := face.Kern(r0, r1); kern != 0 {
return kern
}
}
return 0
}
func (c *CompositeFace) Metrics() font.Metrics {
if len(c.Faces) == 0 {
return font.Metrics{}
}
return c.Faces[0].Metrics()
}
// 获取实际字符渲染用的字体的 Metrics
// func (c *CompositeFace) ActualMetrics(r rune) font.Metrics {
// // 查找实际渲染该字符的字体
// for _, f := range c.Faces {
// if _, _, ok := f.GlyphBounds(r); ok {
// return f.Metrics()
// }
// }
// // 找不到支持字符的字体时返回第一个的 Metrics
// return c.Faces[0].Metrics()
// }
func (c *CompositeFace) Close() error {
var err error
// for _, face := range c.Faces {
// if closer, ok := face.(io.Closer); ok {
// if e := closer.Close(); e != nil {
// err = e
// }
// }
// }
return err
}
type TextOption struct {
Width float64
Height float64
LineHeight float64
PaddingX float64
PaddingY float64
NoWrap bool
Color string
BgColor string
BorderColor string
BorderWidth float64
BorderRadius float64
Align string // left center right
VAlign string // top middle bottom
ShadowColor string // default #333333
ShadowOffset int
}
// 绘制文本
func (g *Graphics) Text(x, y float64, text string, opt *TextOption) {
if opt == nil {
opt = &TextOption{}
}
if opt.LineHeight <= 0.01 {
opt.LineHeight = 1.0
}
// 设置文本颜色
if opt.Color != "" {
g.dc.SetColor(hexToRGBA(opt.Color))
}
if g.lastFont == nil {
g.SetSansSerifFont(14)
}
// 处理换行符
paragraphs := strings.Split(text, "\n")
if strings.Contains(text, "\r\n") {
paragraphs = strings.Split(text, "\r\n")
}
// 准备文本行
var lines []string
hasWrap := !opt.NoWrap && opt.Width > 0
for _, para := range paragraphs {
if para == "" {
lines = append(lines, "") // 保留空行
} else if hasWrap {
// 使用drawWidth计算换行
lines = append(lines, g.wrapParagraph(para, opt.Width-2*opt.PaddingX)...)
} else if opt.Width > 0 {
// NoWrap模式但指定了宽度使用相同的换行逻辑但只取第一行
wrappedLines := g.wrapParagraph(para, opt.Width-2*opt.PaddingX)
if len(wrappedLines) > 0 {
lines = append(lines, wrappedLines[0])
} else {
lines = append(lines, "")
}
} else {
lines = append(lines, para)
}
}
// 计算文本总高度
lineHeight := g.calcLineHeight(opt.LineHeight)
totalHeight := float64(len(lines)) * lineHeight
// 计算字体的上升和下降部分
ascent := 0.0
descent := 0.0
if g.lastFont != nil {
metrics := g.lastFont.Metrics()
ascent = float64(metrics.Ascent) / 64
descent = float64(metrics.Descent) / 64
} else {
// 默认估算
ascent = g.dc.FontHeight() * 0.7
descent = g.dc.FontHeight() * 0.3
}
// 计算文本内容尺寸
textWidth := opt.Width
textHeight := opt.Height
// 计算最大行宽
maxWidth := 0.0
for _, line := range lines {
w, _ := g.dc.MeasureString(line)
if w > maxWidth {
maxWidth = w
}
}
// 如果未指定宽度,计算所需宽度
if textWidth == 0 {
textWidth = maxWidth + 2*opt.PaddingX
}
// 如果未指定高度,计算所需高度
if textHeight == 0 {
textHeight = totalHeight + 2*opt.PaddingY
}
var halfOffset float64
if opt.ShadowOffset != 0 {
if opt.ShadowColor == "" {
opt.ShadowColor = "#333333"
}
halfOffset = float64(opt.ShadowOffset) / 2
}
// 绘制背景和边框(在文本之前)
if opt.BgColor != "" || opt.BorderColor != "" {
if opt.ShadowOffset != 0 {
g.dc.SetColor(hexToRGBA(opt.ShadowColor))
if opt.BorderRadius > 0 {
g.dc.DrawRoundedRectangle(x+halfOffset*2, y+halfOffset*2, textWidth, textHeight, opt.BorderRadius)
} else {
g.dc.DrawRectangle(x+halfOffset*2, y+halfOffset*2, textWidth, textHeight)
}
if opt.BgColor != "" {
g.dc.Fill()
} else {
g.dc.Stroke()
}
}
// 绘制背景
if opt.BgColor != "" {
g.dc.SetColor(hexToRGBA(opt.BgColor))
if opt.BorderRadius > 0 {
g.dc.DrawRoundedRectangle(x, y, textWidth, textHeight, opt.BorderRadius)
} else {
g.dc.DrawRectangle(x, y, textWidth, textHeight)
}
g.dc.Fill()
}
// 绘制边框
if opt.BorderWidth < 0.01 {
opt.BorderWidth = 1
}
if opt.BorderColor != "" {
g.dc.SetColor(hexToRGBA(opt.BorderColor))
g.dc.SetLineWidth(opt.BorderWidth)
if opt.BorderRadius > 0 {
g.dc.DrawRoundedRectangle(x, y, textWidth, textHeight, opt.BorderRadius)
} else {
g.dc.DrawRectangle(x, y, textWidth, textHeight)
}
g.dc.Stroke()
}
}
// 计算实际内容和绘制区域
contentX := x + opt.PaddingX
contentY := y + opt.PaddingY
drawWidth := textWidth - 2*opt.PaddingX
drawHeight := textHeight - 2*opt.PaddingY
// 修正垂直对齐计算
// startY := contentY + ascent + descent // 基线位置
startY := contentY + ascent + (lineHeight-ascent-descent)/2 // 基线位置
if drawHeight > 0 {
// 计算文本块的实际高度
textBlockHeight := totalHeight + descent
switch opt.VAlign {
case "top":
// 顶部对齐:从顶部开始绘制
case "middle":
// 中间对齐:居中位置减去文本块高度的一半
startY += (drawHeight - textBlockHeight) / 2
case "bottom":
// 底部对齐:从底部减去文本块高度
startY += drawHeight - textBlockHeight
}
}
// 创建阴影效果(如果有)
if opt.ShadowOffset != 0 {
g.dc.SetColor(hexToRGBA(opt.ShadowColor))
for i, line := range lines {
// 计算行位置
lineY := startY + float64(i)*lineHeight
// 检查行是否在可见区域内
if drawHeight > 0 && (lineY < contentY || lineY > contentY+drawHeight) {
continue
}
lineX := g.calcLineX(contentX, drawWidth, line, opt.Align)
g.dc.DrawString(line, lineX+halfOffset, lineY+halfOffset)
}
}
// 设置文本颜色
if opt.Color != "" {
g.dc.SetColor(hexToRGBA(opt.Color))
}
// 绘制文本
for i, line := range lines {
if line == "" {
continue
}
// 计算行位置
lineY := startY + float64(i)*lineHeight
// 检查行是否在可见区域内
if drawHeight > 0 && (lineY < contentY || lineY > contentY+drawHeight) {
continue
}
lineX := g.calcLineX(contentX, drawWidth, line, opt.Align)
g.dc.DrawString(line, lineX-halfOffset, lineY-halfOffset)
}
}
// 计算行水平位置
func (g *Graphics) calcLineX(x, w float64, text, align string) float64 {
if w <= 0 {
return x
}
switch align {
case "center":
textW, _ := g.dc.MeasureString(text)
return x + (w-textW)/2
case "right":
textW, _ := g.dc.MeasureString(text)
return x + w - textW
default: // left
return x
}
}
// 段落换行算法
func (g *Graphics) wrapParagraph(para string, maxWidth float64) []string {
var lines []string
currentLine := ""
currentWidth := 0.0
for _, r := range para {
char := string(r)
// 快速估算字符宽度
charWidth := g.estimateCharWidth(r)
if currentWidth+charWidth <= maxWidth {
currentLine += char
currentWidth += charWidth
continue
}
// 精确测量(必要时)
if measuredWidth, _ := g.dc.MeasureString(currentLine + char); measuredWidth <= maxWidth {
currentLine += char
currentWidth = measuredWidth
continue
}
// 需要换行处理
if currentLine != "" {
lines = append(lines, currentLine)
}
currentLine = char
currentWidth = g.estimateCharWidth(r)
}
if currentLine != "" {
lines = append(lines, currentLine)
}
return lines
}
// 字符宽度估算(比实际测量高效)
func (g *Graphics) estimateCharWidth(r rune) float64 {
if g.lastFont != nil {
advance, ok := g.lastFont.GlyphAdvance(r)
if ok {
return float64(advance) / 64 // fixed.Int26_6 转像素值
}
}
return g.dc.FontHeight() * 0.6 // 默认比例估算
}
// 行高计算
func (g *Graphics) calcLineHeight(multiplier float64) float64 {
if multiplier <= 0 {
multiplier = 1.0 // 默认行高
}
if g.lastFont != nil {
metrics := g.lastFont.Metrics()
return float64(metrics.Height) / 64 * multiplier
}
return g.dc.FontHeight() * multiplier
}
// MeasureText 测量文本尺寸
func (g *Graphics) MeasureText(text string) (width, height float64) {
return g.dc.MeasureString(text)
}
// RandText 绘制随机文本并返回每个字符的位置信息
// 返回: 每个字符的位置数组 [x, y, width, height]
func (g *Graphics) RandText(text string) [][4]float64 {
if text == "" {
return nil
}
bounds := g.dc.Image().Bounds()
w, h := bounds.Dx(), bounds.Dy()
// 智能字体大小计算:确保字体足够大
fontSize := math.Max(32, float64(h)*0.6) // 最小32px高度60%
if len(text) > 4 {
fontSize = math.Max(28, fontSize*0.9) // 长文本适当减小
}
g.SetFont(fontSize)
metrics := g.lastFont.Metrics()
ascent := float64(metrics.Ascent) / 64
descent := float64(metrics.Descent) / 64
charHeight := ascent + descent
// height := float64(metrics.Height) / 64
fullWidth, _ := g.dc.MeasureString(text)
// 计算初始位置(水平居中,但留出随机偏移空间)
startX := (float64(w) - fullWidth) / 2
startY := float64(h)/2 + ascent - charHeight/2 // 基于文本高度的垂直居中
// 保存初始状态
g.dc.Push()
defer g.dc.Pop()
// 存储每个字符的位置信息
charPositions := make([][4]float64, 0, len(text))
// 字符处理
x := startX
for _, char := range text {
charStr := string(char)
// 准确测量中文字符
charWidth, _ := g.dc.MeasureString(charStr)
// charWidth, charHeight := g.dc.MeasureString(charStr)
// 随机Y偏移不超过字体高度的20%
maxYOffset := fontSize * 0.2
yOffset := rand.Float64()*(2*maxYOffset) - maxYOffset
// 随机旋转(-15°到15°
angle := rand.Float64()*0.52 - 0.26 // ±15°弧度值
// 记录字符位置(旋转前的原始位置)
charPositions = append(charPositions, [4]float64{
x, // X位置
startY + yOffset - charHeight + descent, // Y位置
charWidth, // 字符宽度
charHeight, // 字符高度
})
g.dc.Push()
// 应用变换
g.dc.RotateAbout(angle, x, startY+yOffset)
g.dc.SetColor(color.Gray{Y: 50})
g.dc.DrawString(charStr, x+1, startY+yOffset+1)
g.dc.SetColor(generateLightTextColor())
g.dc.DrawString(charStr, x, startY+yOffset)
g.dc.Pop()
// 优化间距:确保足够的字符间距
// 基础间距为字符宽度的80%
baseSpace := charWidth * 0.8
// 添加随机间距0-20%
randomSpace := rand.Float64() * charWidth * 0.2
// 长文本适当减少间距
if len(text) > 6 {
baseSpace *= 0.9
}
// 根据角度调整实际水平移动
effectiveSpace := (baseSpace + randomSpace) * math.Cos(math.Abs(angle))
x += effectiveSpace * 1.2
}
return charPositions
}
// 生成浅色系文字颜色
func generateLightTextColor() color.RGBA {
return color.RGBA{
R: 150 + uint8(rand.Intn(106)),
G: 150 + uint8(rand.Intn(106)),
B: 150 + uint8(rand.Intn(106)),
A: 190 + uint8(rand.Intn(66)),
}
}