1
This commit is contained in:
commit
cc31aa8248
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.*
|
||||
!.gitignore
|
||||
go.sum
|
||||
package.json
|
||||
node_modules
|
847
draw.go
Normal file
847
draw.go
Normal 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
319
effect.go
Normal 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
28
go.mod
Normal 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
332
img.go
Normal 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
27
img_test.go
Normal 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
461
img_test.js
Normal 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("测试图像生成完成!")
|
BIN
testimg/全面绘图测试.png
Normal file
BIN
testimg/全面绘图测试.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
731
text.go
Normal file
731
text.go
Normal 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)),
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user