Skip to content

Commit 7864cd0

Browse files
committed
feat: 修复emoji表情类生成
1 parent cda91fb commit 7864cd0

10 files changed

+182
-118
lines changed

.github/workflows/goreleaser.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Release
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
permissions:
7+
contents: write
8+
jobs:
9+
goreleaser:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- name: Set up Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version: 'stable'
20+
- name: Run GoReleaser
21+
uses: goreleaser/goreleaser-action@v6
22+
with:
23+
version: latest
24+
args: release --clean
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/golangci-lint.yml .github/workflows/lint.yml

+3-6
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- uses: actions/checkout@v4
15-
- uses: actions/setup-go@v4
15+
- uses: actions/setup-go@v5
1616
with:
1717
go-version: 'stable'
18-
cache: false
1918
- name: Lint
20-
uses: golangci/golangci-lint-action@v3
19+
uses: golangci/golangci-lint-action@v6
2120
with:
2221
skip-cache: true
23-
skip-pkg-cache: true
24-
skip-build-cache: true
2522
version: latest
26-
args: --timeout=30m ./...
23+
args: --timeout=30m ./...

.github/workflows/test.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ jobs:
88
test:
99
strategy:
1010
matrix:
11-
go: [ '1.21', '1.20' ]
12-
runs-on: ubuntu-latest
11+
go: [ 'stable', 'oldstable' ]
12+
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
13+
runs-on: ${{ matrix.os }}
1314
steps:
1415
- uses: actions/checkout@v4
1516
- uses: actions/setup-go@v4
@@ -18,4 +19,4 @@ jobs:
1819
- name: Install dependencies
1920
run: go mod tidy
2021
- name: Run tests
21-
run: go test ./...
22+
run: go test ./...

.goreleaser.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version: 2
2+
builds:
3+
- skip: true

draw.go

+99-70
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
package letteravatar
33

44
import (
5+
"fmt"
6+
"hash/fnv"
57
"image"
68
"image/color"
79
"image/draw"
8-
"math/rand"
9-
"sync"
10-
"time"
10+
"math/rand/v2"
1111

12-
"github.com/goki/freetype"
13-
"github.com/goki/freetype/truetype"
12+
"golang.org/x/image/font"
13+
"golang.org/x/image/font/opentype"
1414
"golang.org/x/image/math/fixed"
1515
)
1616

1717
// Options are letter-avatar parameters.
1818
type Options struct {
19-
Font *truetype.Font
19+
Font *opentype.Font
2020
Palette []color.Color
2121
LetterColor color.Color
2222
FontSize int
@@ -27,47 +27,40 @@ type Options struct {
2727
PaletteKey string
2828
}
2929

30-
var defaultLetterColor = color.RGBA{0xf0, 0xf0, 0xf0, 0xf0}
30+
var defaultLetterColor = color.RGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xf0}
3131

3232
// Draw generates a new letter-avatar image of the given size using the given letter
3333
// with the given options. Default parameters are used if a nil *Options is passed.
3434
func Draw(size int, letters []rune, options *Options) (image.Image, error) {
35-
font := defaultFont
36-
if options != nil && options.Font != nil {
37-
font = options.Font
35+
if options == nil {
36+
options = &Options{}
3837
}
39-
40-
palette := defaultPalette
41-
if options != nil && options.Palette != nil {
42-
palette = options.Palette
38+
if options.Font == nil {
39+
options.Font = defaultFont
4340
}
44-
45-
var letterColor color.Color = defaultLetterColor
46-
if options != nil && options.LetterColor != nil {
47-
letterColor = options.LetterColor
41+
if options.Palette == nil {
42+
options.Palette = defaultPalette
43+
}
44+
if options.LetterColor == nil {
45+
options.LetterColor = defaultLetterColor
4846
}
4947

50-
var bgColor color.Color = color.RGBA{0x00, 0x00, 0x00, 0xff}
51-
if len(palette) > 0 {
52-
if options != nil && len(options.PaletteKey) > 0 {
53-
bgColor = palette[keyindex(len(palette), options.PaletteKey)]
48+
var bgColor color.Color = color.RGBA{A: 0xff}
49+
if len(options.Palette) > 0 {
50+
if len(options.PaletteKey) > 0 {
51+
bgColor = options.Palette[randomIndex(len(options.Palette), options.PaletteKey)]
5452
} else {
55-
bgColor = palette[randint(len(palette))]
53+
bgColor = options.Palette[rand.IntN(len(options.Palette))]
5654
}
5755
}
5856

59-
fontSize := float64(options.FontSize)
60-
if options.FontSize == 0 {
61-
fontSize = float64(size) * 0.6
62-
}
63-
64-
return drawAvatar(bgColor, letterColor, font, size, fontSize, letters)
57+
return drawAvatar(bgColor, options.LetterColor, options.Font, size, float64(options.FontSize), letters)
6558
}
6659

67-
func drawAvatar(bgColor, fgColor color.Color, font *truetype.Font, size int, fontSize float64, letters []rune) (image.Image, error) {
60+
func drawAvatar(bgColor, fgColor color.Color, f *opentype.Font, size int, fontSize float64, letters []rune) (image.Image, error) {
6861
dst := newRGBA(size, size, bgColor)
6962

70-
src, err := drawString(bgColor, fgColor, font, fontSize, letters)
63+
src, err := drawString(bgColor, fgColor, f, size, fontSize, letters)
7164
if err != nil {
7265
return nil, err
7366
}
@@ -78,60 +71,96 @@ func drawAvatar(bgColor, fgColor color.Color, font *truetype.Font, size int, fon
7871
return dst, nil
7972
}
8073

81-
func drawString(bgColor, fgColor color.Color, font *truetype.Font, fontSize float64, letters []rune) (image.Image, error) {
82-
c := freetype.NewContext()
83-
c.SetDPI(72)
74+
func drawString(bgColor, fgColor color.Color, f *opentype.Font, size int, fontSize float64, letters []rune) (image.Image, error) {
75+
// 自动计算字体大小
76+
if fontSize <= 0 {
77+
fontSize = calculateFontSize(len(letters), size)
78+
}
8479

85-
bb := font.Bounds(c.PointToFixed(fontSize))
86-
w := bb.Max.X.Ceil() - bb.Min.X.Floor()
87-
h := bb.Max.Y.Ceil() - bb.Min.Y.Floor()
88-
if len(letters) > 0 {
89-
w = w + int(fontSize)*(len(letters)-1)
80+
face, err := opentype.NewFace(f, &opentype.FaceOptions{
81+
Size: fontSize,
82+
DPI: 72,
83+
Hinting: font.HintingFull,
84+
})
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to create font face: %v", err)
9087
}
88+
defer face.Close()
9189

92-
dst := newRGBA(w, h, bgColor)
93-
src := image.NewUniform(fgColor)
90+
// 计算文本尺寸以居中显示
91+
textWidth, ascent, descent, _ := calculateTextDimensions(face, string(letters))
9492

95-
c.SetDst(dst)
96-
c.SetSrc(src)
97-
c.SetClip(dst.Bounds())
98-
c.SetFontSize(fontSize)
99-
c.SetFont(font)
93+
x := (size - textWidth) / 2
94+
y := size/2 + (ascent-descent)/2
10095

101-
p, err := c.DrawString(string(letters), fixed.Point26_6{X: 0, Y: bb.Max.Y})
102-
if err != nil {
103-
return nil, err
96+
dst := newRGBA(x, y, bgColor)
97+
drawText(dst, face, string(letters), x, y, fgColor)
98+
99+
return dst, nil
100+
}
101+
102+
// calculateFontSize 根据图像大小和文本长度确定最佳字体大小
103+
func calculateFontSize(textLength int, imageSize int) float64 {
104+
// 以图像大小的2/3作为初始大小估计
105+
initialSize := float64(imageSize) * 2.0 / 3.0
106+
107+
// 根据文本长度调整
108+
if textLength > 1 {
109+
initialSize = initialSize * 3.0 / float64(textLength+2)
110+
}
111+
112+
// 确保字体大小至少为12px且不超过图像大小
113+
if initialSize < 12 {
114+
initialSize = 12
115+
} else if initialSize > float64(imageSize) {
116+
initialSize = float64(imageSize)
104117
}
105118

106-
return dst.SubImage(image.Rect(0, 0, p.X.Ceil(), h)), nil
119+
return initialSize
107120
}
108121

109-
func newRGBA(w, h int, c color.Color) *image.RGBA {
110-
img := image.NewRGBA(image.Rect(0, 0, w, h))
111-
rgba := color.RGBAModel.Convert(c).(color.RGBA)
112-
for x := 0; x < w; x++ {
113-
for y := 0; y < h; y++ {
114-
img.SetRGBA(x, y, rgba)
122+
// calculateTextDimensions 计算文本的宽度和高度,返回宽度、上升部分高度、下降部分高度和行间距
123+
func calculateTextDimensions(face font.Face, text string) (width int, ascent int, descent int, lineGap int) {
124+
metrics := face.Metrics()
125+
ascent = metrics.Ascent.Ceil()
126+
descent = metrics.Descent.Ceil()
127+
lineGap = metrics.Height.Ceil() - ascent - descent
128+
129+
// 计算文本宽度
130+
var w fixed.Int26_6
131+
for _, r := range text {
132+
adv, ok := face.GlyphAdvance(r)
133+
if !ok {
134+
adv, _ = face.GlyphAdvance(' ') // 如果字符不支持,使用空格的宽度
115135
}
136+
w += adv
116137
}
117-
return img
138+
139+
return w.Ceil(), ascent, descent, lineGap
118140
}
119141

120-
func keyindex(n int, key string) int {
121-
var index int64
122-
for _, r := range key {
123-
index = (index + int64(r)) % int64(n)
142+
// drawText 将文本绘制到图像上
143+
func drawText(img *image.RGBA, face font.Face, text string, x, y int, textColor color.Color) {
144+
d := &font.Drawer{
145+
Dst: img,
146+
Src: image.NewUniform(textColor),
147+
Face: face,
148+
Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)},
124149
}
125-
return int(index)
150+
d.DrawString(text)
126151
}
127152

128-
var (
129-
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
130-
rngMu = new(sync.Mutex)
131-
)
153+
func newRGBA(w, h int, c color.Color) *image.RGBA {
154+
img := image.NewRGBA(image.Rect(0, 0, w, h))
155+
draw.Draw(img, img.Bounds(), image.NewUniform(c), image.Point{}, draw.Src)
156+
return img
157+
}
132158

133-
func randint(n int) int {
134-
rngMu.Lock()
135-
defer rngMu.Unlock()
136-
return rng.Intn(n)
159+
func randomIndex(n int, key string) int {
160+
h := fnv.New64a()
161+
if _, err := h.Write([]byte(key)); err != nil {
162+
return 0
163+
}
164+
rd := rand.New(rand.NewPCG(h.Sum64(), (h.Sum64()>>1)|1))
165+
return rd.IntN(n)
137166
}

draw_test.go

+5-27
Large diffs are not rendered by default.

font.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ import (
213213
"compress/gzip"
214214
"io"
215215

216-
"github.com/goki/freetype/truetype"
216+
"golang.org/x/image/font/opentype"
217217
)
218218

219-
var defaultFont *truetype.Font
219+
var defaultFont *opentype.Font
220220

221221
func init() {
222222
gz, err := gzip.NewReader(bytes.NewBuffer(fontData))
@@ -231,7 +231,7 @@ func init() {
231231
panic(err)
232232
}
233233

234-
defaultFont, err = truetype.Parse(buf.Bytes())
234+
defaultFont, err = opentype.Parse(buf.Bytes())
235235
if err != nil {
236236
panic(err)
237237
}

go.mod

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
module github.com/tnb-labs/letteravatar
22

3-
go 1.20
3+
go 1.23.0
44

5-
require (
6-
github.com/goki/freetype v1.0.1
7-
golang.org/x/image v0.13.0
8-
)
5+
require golang.org/x/image v0.25.0
6+
7+
require golang.org/x/text v0.23.0 // indirect

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
github.com/goki/freetype v1.0.1 h1:10DgpEu+QEh/hpvAxgx//RT8ayWwHJI+nZj3QNcn8uk=
2-
github.com/goki/freetype v1.0.1/go.mod h1:ni9Dgz8vA6o+13u1Ke0q3kJcCJ9GuXb1dtlfKho98vs=
3-
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
4-
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
1+
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
2+
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
3+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
4+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=

renovate.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"config:recommended"
5+
],
6+
"labels": [
7+
"🤖 Dependencies"
8+
],
9+
"commitMessagePrefix": "chore(deps): ",
10+
"lockFileMaintenance": {
11+
"enabled": true,
12+
"automerge": true
13+
},
14+
"platformAutomerge": true,
15+
"postUpdateOptions": [
16+
"gomodTidy"
17+
],
18+
"packageRules": [
19+
{
20+
"groupName": "non-major dependencies",
21+
"matchUpdateTypes": [
22+
"digest",
23+
"pin",
24+
"patch",
25+
"minor"
26+
],
27+
"automerge": true
28+
}
29+
],
30+
"ignoreDeps": []
31+
}

0 commit comments

Comments
 (0)