Skip to content

Commit 59288a1

Browse files
committed
refactor: split utils.go
1 parent 5aa2413 commit 59288a1

8 files changed

Lines changed: 251 additions & 354 deletions

File tree

color.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ var (
2626
Opaque = color.RGBA{0, 0, 0, 0}
2727
)
2828

29-
// takeThemeColorsKMeans 实现基于k-means算法的图像取色算法
30-
func takeThemeColorsKMeans(img image.Image, k int) []color.RGBA {
29+
// TakeThemeColorsKMeans extracts the k dominant colors from an image using k-means.
30+
//
31+
// TakeThemeColorsKMeans 使用 k-means 算法从图像中提取 k 个主色。
32+
func TakeThemeColorsKMeans(img image.Image, k int) []color.RGBA {
3133
rgbaimg := ImageToRGBA(img)
3234
pixels := unsafe.Slice(
3335
(*color.RGBA)(unsafe.Pointer(unsafe.SliceData(rgbaimg.Pix))),
@@ -57,11 +59,11 @@ func takeThemeColorsKMeans(img image.Image, k int) []color.RGBA {
5759

5860
// 计算每个聚类的新中心
5961
newClusters := make([]color.RGBA, k)
60-
for i := range k {
62+
for currentCluster := range k {
6163
var r, g, b uint32
6264
n := 0
63-
for j, cluster := range clusterAssignments {
64-
if cluster == i {
65+
for j, pixelCluster := range clusterAssignments {
66+
if pixelCluster == currentCluster {
6567
pixel := pixels[j]
6668
r += uint32(pixel.R)
6769
g += uint32(pixel.G)
@@ -70,12 +72,12 @@ func takeThemeColorsKMeans(img image.Image, k int) []color.RGBA {
7072
}
7173
}
7274
if n != 0 {
73-
newClusters[i] = color.RGBA{uint8(r / uint32(n)), uint8(g / uint32(n)), uint8(b / uint32(n)), 255}
75+
newClusters[currentCluster] = color.RGBA{uint8(r / uint32(n)), uint8(g / uint32(n)), uint8(b / uint32(n)), 255}
7476
}
7577
}
7678

7779
// 如果聚类中心没有变化,则停止迭代
78-
if clustersEqual(clusters, newClusters) {
80+
if isArrayRGBAEqual(clusters, newClusters) {
7981
break
8082
}
8183
clusters = newClusters
@@ -84,6 +86,35 @@ func takeThemeColorsKMeans(img image.Image, k int) []color.RGBA {
8486
return clusters
8587
}
8688

89+
// isArrayRGBAEqual compares two []color.RGBA is equal fastly.
90+
//
91+
// isArrayRGBAEqual 快速比较两个 []color.RGBA 是否相等。
92+
func isArrayRGBAEqual(a, b []color.RGBA) bool {
93+
if len(a) != len(b) {
94+
return false
95+
}
96+
sz := len(a)
97+
if sz%2 == 0 { // can compare by uint64
98+
u64a := unsafe.Slice((*uint64)(unsafe.Pointer(unsafe.SliceData(a))), sz/2)
99+
u64b := unsafe.Slice((*uint64)(unsafe.Pointer(unsafe.SliceData(b))), sz/2)
100+
for i := range u64a {
101+
if u64a[i] != u64b[i] {
102+
return false
103+
}
104+
}
105+
return true
106+
}
107+
// compare by uint32
108+
u32a := unsafe.Slice((*uint32)(unsafe.Pointer(unsafe.SliceData(a))), sz)
109+
u32b := unsafe.Slice((*uint32)(unsafe.Pointer(unsafe.SliceData(b))), sz)
110+
for i := range u32a {
111+
if u32a[i] != u32b[i] {
112+
return false
113+
}
114+
}
115+
return true
116+
}
117+
87118
// 计算两个颜色之间的距离
88119
func distance(a, b color.RGBA) float64 {
89120
return math.Sqrt(sq(float64(a.R)-float64(b.R)) + sq(float64(a.G)-float64(b.G)) + sq(float64(a.B)-float64(b.B)))
@@ -93,16 +124,3 @@ func distance(a, b color.RGBA) float64 {
93124
func sq(n float64) float64 {
94125
return n * n
95126
}
96-
97-
// 比较两个聚类中心是否相等
98-
func clustersEqual(a, b []color.RGBA) bool {
99-
if len(a) != len(b) {
100-
return false
101-
}
102-
for i := range a {
103-
if a[i] != b[i] {
104-
return false
105-
}
106-
}
107-
return true
108-
}

color_test.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -160,44 +160,44 @@ func TestDistance_IgnoresAlpha(t *testing.T) {
160160
func TestClustersEqual_Equal(t *testing.T) {
161161
a := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}}
162162
b := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}}
163-
if !clustersEqual(a, b) {
163+
if !isArrayRGBAEqual(a, b) {
164164
t.Error("clustersEqual should return true for identical slices")
165165
}
166166
}
167167

168168
func TestClustersEqual_NotEqual(t *testing.T) {
169169
a := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}}
170170
b := []color.RGBA{{255, 0, 0, 255}, {0, 0, 255, 255}}
171-
if clustersEqual(a, b) {
171+
if isArrayRGBAEqual(a, b) {
172172
t.Error("clustersEqual should return false for different slices")
173173
}
174174
}
175175

176176
func TestClustersEqual_DifferentLength(t *testing.T) {
177177
a := []color.RGBA{{255, 0, 0, 255}}
178178
b := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}}
179-
if clustersEqual(a, b) {
179+
if isArrayRGBAEqual(a, b) {
180180
t.Error("clustersEqual should return false for slices of different lengths")
181181
}
182182
}
183183

184184
func TestClustersEqual_Empty(t *testing.T) {
185-
if !clustersEqual([]color.RGBA{}, []color.RGBA{}) {
185+
if !isArrayRGBAEqual([]color.RGBA{}, []color.RGBA{}) {
186186
t.Error("clustersEqual should return true for two empty slices")
187187
}
188188
}
189189

190190
func TestClustersEqual_OneEmpty(t *testing.T) {
191191
a := []color.RGBA{{255, 0, 0, 255}}
192-
if clustersEqual(a, []color.RGBA{}) {
192+
if isArrayRGBAEqual(a, []color.RGBA{}) {
193193
t.Error("clustersEqual should return false when one slice is empty")
194194
}
195195
}
196196

197197
func TestClustersEqual_SingleElement(t *testing.T) {
198198
a := []color.RGBA{{100, 100, 100, 255}}
199199
b := []color.RGBA{{100, 100, 100, 255}}
200-
if !clustersEqual(a, b) {
200+
if !isArrayRGBAEqual(a, b) {
201201
t.Error("clustersEqual should return true for identical single-element slices")
202202
}
203203
}
@@ -206,7 +206,7 @@ func TestClustersEqual_DiffersOnlyInAlpha(t *testing.T) {
206206
a := []color.RGBA{{100, 100, 100, 255}}
207207
b := []color.RGBA{{100, 100, 100, 0}}
208208
// RGBA 结构体逐字段比较,Alpha 也参与比较
209-
if clustersEqual(a, b) {
209+
if isArrayRGBAEqual(a, b) {
210210
t.Error("clustersEqual should return false when alpha differs")
211211
}
212212
}
@@ -216,7 +216,7 @@ func TestClustersEqual_DiffersOnlyInAlpha(t *testing.T) {
216216
func TestTakecolor_ReturnsKColors(t *testing.T) {
217217
img := solidImage(10, 10, color.RGBA{128, 64, 32, 255})
218218
for k := 1; k <= 5; k++ {
219-
result := takeThemeColorsKMeans(img, k)
219+
result := TakeThemeColorsKMeans(img, k)
220220
if len(result) != k {
221221
t.Errorf("takecolor with k=%d returned %d colors, want %d", k, len(result), k)
222222
}
@@ -226,7 +226,7 @@ func TestTakecolor_ReturnsKColors(t *testing.T) {
226226
func TestTakecolor_SolidColorK1(t *testing.T) {
227227
c := color.RGBA{200, 100, 50, 255}
228228
img := solidImage(20, 20, c)
229-
result := takeThemeColorsKMeans(img, 1)
229+
result := TakeThemeColorsKMeans(img, 1)
230230
if len(result) != 1 {
231231
t.Fatalf("expected 1 color, got %d", len(result))
232232
}
@@ -242,7 +242,7 @@ func TestTakecolor_SolidColorKGreaterThan1(t *testing.T) {
242242
// 故只验证:返回 k 个颜色,且其中至少一个与原始颜色完全匹配。
243243
c := color.RGBA{10, 200, 150, 255}
244244
img := solidImage(15, 15, c)
245-
result := takeThemeColorsKMeans(img, 3)
245+
result := TakeThemeColorsKMeans(img, 3)
246246
if len(result) != 3 {
247247
t.Fatalf("expected 3 colors, got %d", len(result))
248248
}
@@ -264,7 +264,7 @@ func TestTakecolor_TwoDistinctColors(t *testing.T) {
264264
img := twoColorImage(20, 20, Red, Blue)
265265
const maxAttempts = 30
266266
for range maxAttempts {
267-
result := takeThemeColorsKMeans(img, 2)
267+
result := TakeThemeColorsKMeans(img, 2)
268268
if len(result) == 2 && colorInSlice(Red, result, 5) && colorInSlice(Blue, result, 5) {
269269
return // 成功分离,测试通过
270270
}
@@ -276,9 +276,9 @@ func TestTakecolor_Deterministic_SolidImage(t *testing.T) {
276276
// 纯色图像下,无论随机种子如何,结果应完全一致
277277
c := color.RGBA{77, 88, 99, 255}
278278
img := solidImage(10, 10, c)
279-
r1 := takeThemeColorsKMeans(img, 2)
280-
r2 := takeThemeColorsKMeans(img, 2)
281-
if !clustersEqual(r1, r2) {
279+
r1 := TakeThemeColorsKMeans(img, 2)
280+
r2 := TakeThemeColorsKMeans(img, 2)
281+
if !isArrayRGBAEqual(r1, r2) {
282282
t.Errorf("takecolor on solid image should be deterministic: r1=%v, r2=%v", r1, r2)
283283
}
284284
}
@@ -296,7 +296,7 @@ func TestTakecolor_AllClustersHaveValidRGB(t *testing.T) {
296296
})
297297
}
298298
}
299-
result := takeThemeColorsKMeans(img, 4)
299+
result := TakeThemeColorsKMeans(img, 4)
300300
if len(result) != 4 {
301301
t.Fatalf("expected 4 clusters, got %d", len(result))
302302
}
@@ -329,7 +329,7 @@ func BenchmarkClustersEqual_Equal(b *testing.B) {
329329
c := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {128, 128, 128, 255}}
330330
b.ResetTimer()
331331
for range b.N {
332-
clustersEqual(a, c)
332+
isArrayRGBAEqual(a, c)
333333
}
334334
}
335335

@@ -338,7 +338,7 @@ func BenchmarkClustersEqual_NotEqual(b *testing.B) {
338338
c := []color.RGBA{{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {200, 200, 200, 255}}
339339
b.ResetTimer()
340340
for range b.N {
341-
clustersEqual(a, c)
341+
isArrayRGBAEqual(a, c)
342342
}
343343
}
344344

@@ -352,7 +352,7 @@ func BenchmarkTakecolor_16x16_K3(b *testing.B) {
352352
}
353353
b.ResetTimer()
354354
for range b.N {
355-
takeThemeColorsKMeans(img, 3)
355+
TakeThemeColorsKMeans(img, 3)
356356
}
357357
}
358358

@@ -366,7 +366,7 @@ func BenchmarkTakecolor_64x64_K4(b *testing.B) {
366366
}
367367
b.ResetTimer()
368368
for range b.N {
369-
takeThemeColorsKMeans(img, 4)
369+
TakeThemeColorsKMeans(img, 4)
370370
}
371371
}
372372

@@ -380,14 +380,14 @@ func BenchmarkTakecolor_128x128_K8(b *testing.B) {
380380
}
381381
b.ResetTimer()
382382
for range b.N {
383-
takeThemeColorsKMeans(img, 8)
383+
TakeThemeColorsKMeans(img, 8)
384384
}
385385
}
386386

387387
func BenchmarkTakecolor_SolidColor_K5(b *testing.B) {
388388
img := solidImage(64, 64, color.RGBA{200, 100, 50, 255})
389389
b.ResetTimer()
390390
for range b.N {
391-
takeThemeColorsKMeans(img, 5)
391+
TakeThemeColorsKMeans(img, 5)
392392
}
393393
}

context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1366,5 +1366,5 @@ func (dc *Context) String() string {
13661366
//
13671367
// TakeThemeColorsKMeans 使用 k-means 算法从已绘制图像中提取 k 个主色。
13681368
func (dc *Context) TakeThemeColorsKMeans(k int) []color.RGBA {
1369-
return takeThemeColorsKMeans(dc.im, k)
1369+
return TakeThemeColorsKMeans(dc.im, k)
13701370
}

font.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package gg
2+
3+
import (
4+
"os"
5+
6+
"golang.org/x/image/font"
7+
"golang.org/x/image/font/opentype"
8+
)
9+
10+
// LoadFontFace is a helper function to load the specified font file with
11+
// the specified point size. Note that the returned `font.Face` objects
12+
// are not thread safe and cannot be used in parallel across goroutines.
13+
// You can usually just use the Context.LoadFontFace function instead of
14+
// this package-level function.
15+
//
16+
// LoadFontFace 是一个辅助函数,用于加载指定点大小的指定字体文件。
17+
// 请注意,返回的 `font.Face` 对象不是线程安全的,不能跨 goroutine 并行使用。
18+
// 您通常可以只使用 Context.LoadFontFace 函数而不是这个包级函数。
19+
func LoadFontFace(path string, points float64) (face font.Face, err error) {
20+
fontBytes, err := os.ReadFile(path)
21+
if err != nil {
22+
return
23+
}
24+
f, err := opentype.ParseCollection(fontBytes)
25+
if err != nil {
26+
return
27+
}
28+
fnf, err := f.Font(0)
29+
if err != nil {
30+
return
31+
}
32+
face, err = opentype.NewFace(fnf, &opentype.FaceOptions{
33+
Size: points,
34+
DPI: 72,
35+
// Hinting: font.HintingFull,
36+
})
37+
return
38+
}
39+
40+
// ParseFontFace 是一个辅助函数,用于加载指定点大小的指定字体文件。
41+
// 请注意,返回的 `font.Face` 对象不是线程安全的,不能跨 goroutine 并行使用。
42+
// 您通常可以只使用 Context.LoadFontFace 函数而不是这个包级函数。
43+
func ParseFontFace(b []byte, points float64) (face font.Face, err error) {
44+
f, err := opentype.ParseCollection(b)
45+
if err != nil {
46+
return
47+
}
48+
fnf, err := f.Font(0)
49+
if err != nil {
50+
return
51+
}
52+
face, err = opentype.NewFace(fnf, &opentype.FaceOptions{
53+
Size: points,
54+
DPI: 72,
55+
// Hinting: font.HintingFull,
56+
})
57+
return
58+
}

image.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package gg
2+
3+
import (
4+
"image"
5+
"os"
6+
7+
"github.com/fumiama/imgsz"
8+
"golang.org/x/image/draw"
9+
)
10+
11+
// ImageToRGBA converts an image.Image to *image.RGBA.
12+
//
13+
// ImageToRGBA 将 image.Image 转换为 *image.RGBA。
14+
func ImageToRGBA(src image.Image) *image.RGBA {
15+
bounds := src.Bounds()
16+
dst := image.NewRGBA(bounds)
17+
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
18+
return dst
19+
}
20+
21+
// ImageToRGBA64 converts an image.Image to *image.RGBA64.
22+
//
23+
// ImageToRGBA64 将 image.Image 转换为 *image.RGBA64。
24+
func ImageToRGBA64(src image.Image) *image.RGBA64 {
25+
bounds := src.Bounds()
26+
dst := image.NewRGBA64(bounds)
27+
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
28+
return dst
29+
}
30+
31+
// ImageToNRGBA converts an image.Image to *image.NRGBA.
32+
//
33+
// ImageToNRGBA 将 image.Image 转换为 *image.NRGBA。
34+
func ImageToNRGBA(src image.Image) *image.NRGBA {
35+
bounds := src.Bounds()
36+
dst := image.NewNRGBA(bounds)
37+
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
38+
return dst
39+
}
40+
41+
// ImageToNRGBA64 converts an image.Image to *image.NRGBA64.
42+
//
43+
// ImageToNRGBA64 将 image.Image 转换为 *image.NRGBA64。
44+
func ImageToNRGBA64(src image.Image) *image.NRGBA64 {
45+
bounds := src.Bounds()
46+
dst := image.NewNRGBA64(bounds)
47+
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
48+
return dst
49+
}
50+
51+
// GetImageWxH returns the width and height of the image at the given path.
52+
//
53+
// GetImageWxH 返回指定路径图片的宽度和高度。
54+
func GetImageWxH(path string) (int, int, error) {
55+
f, err := os.Open(path)
56+
if err != nil {
57+
return 0, 0, err
58+
}
59+
defer f.Close()
60+
sz, _, err := imgsz.DecodeSize(f)
61+
return sz.Width, sz.Height, err
62+
}

0 commit comments

Comments
 (0)