commit cc31aa8248f35a2390772fba4f68bb3eafa23aff Author: Star <> Date: Tue Jul 29 16:59:49 2025 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..356d872 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.* +!.gitignore +go.sum +package.json +node_modules diff --git a/draw.go b/draw.go new file mode 100644 index 0000000..a49b05e --- /dev/null +++ b/draw.go @@ -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) + } +} diff --git a/effect.go b/effect.go new file mode 100644 index 0000000..1f410ce --- /dev/null +++ b/effect.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..16b69ff --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/img.go b/img.go new file mode 100644 index 0000000..09492b2 --- /dev/null +++ b/img.go @@ -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) +} diff --git a/img_test.go b/img_test.go new file mode 100644 index 0000000..a00eb50 --- /dev/null +++ b/img_test.go @@ -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) + // } +} diff --git a/img_test.js b/img_test.js new file mode 100644 index 0000000..aa4d972 --- /dev/null +++ b/img_test.js @@ -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("测试图像生成完成!") diff --git a/testimg/全面绘图测试.png b/testimg/全面绘图测试.png new file mode 100644 index 0000000..df9d6ad Binary files /dev/null and b/testimg/全面绘图测试.png differ diff --git a/text.go b/text.go new file mode 100644 index 0000000..195c520 --- /dev/null +++ b/text.go @@ -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)), + } +}