diff --git a/.gitignore b/.gitignore index 66fd13c..474488e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.idea diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 29c29aa..0000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# kenaito-dark-wallpaper-finder - -#### Description -当壁纸数量达到一个量级的时候,人工筛选黑色调的壁纸过于的繁杂,一个好的算法+自动化是最佳的解决方案 - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index ce50fc7..9c1f714 100644 --- a/README.md +++ b/README.md @@ -3,35 +3,7 @@ #### 介绍 当壁纸数量达到一个量级的时候,人工筛选黑色调的壁纸过于的繁杂,一个好的算法+自动化是最佳的解决方案 -#### 软件架构 -软件架构说明 +#### 执行结果 +![1](./doc/QQ20251221-093651.png) - -#### 安装教程 - -1. xxxx -2. xxxx -3. xxxx - -#### 使用说明 - -1. xxxx -2. xxxx -3. xxxx - -#### 参与贡献 - -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request - - -#### 特技 - -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +![1](./doc/QQ20251221-100004.png) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..4f2389b --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +rm -f kenaito-dark-wallpaper-finder.exe +# build +go build -o kenaito-dark-wallpaper-finder.exe main.go diff --git a/doc/QQ20251221-093651.png b/doc/QQ20251221-093651.png new file mode 100644 index 0000000..65454fe Binary files /dev/null and b/doc/QQ20251221-093651.png differ diff --git a/doc/QQ20251221-100004.png b/doc/QQ20251221-100004.png new file mode 100644 index 0000000..2f8b4f4 Binary files /dev/null and b/doc/QQ20251221-100004.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e16e641 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module kenaito-media-group-tool + +go 1.21 + +require ( + github.com/gabriel-vasile/mimetype v1.4.3 + github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d + github.com/satori/go.uuid v1.2.0 + golang.org/x/image v0.16.0 +) + +require ( + golang.org/x/net v0.17.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f923da1 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d h1:h/hohIqMUCML2Rp9BXXAu0I3ZR68d7eqMHLPNq7N2tg= +github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d/go.mod h1:LiYo3EFlYfk46Re4zgQysMo6yO3/kAXslB2fyMdl+uw= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/main.go b/main.go new file mode 100644 index 0000000..42ab8cf --- /dev/null +++ b/main.go @@ -0,0 +1,505 @@ +package main + +import ( + "fmt" + "image" + "image/color" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io/fs" + "log" + "math" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" +) + +// 支持的图片格式 +var supportedExts = map[string]bool{ + ".jpg": true, ".jpeg": true, ".png": true, + ".bmp": true, ".webp": true, ".gif": true, + ".tiff": true, ".tif": true, +} + +// WallpaperInfo 存储壁纸信息 +type WallpaperInfo struct { + Path string + DarkScore float64 // 黑暗程度评分(0-1,越高越暗) + AvgColor color.RGBA + Size int64 + Width int + Height int +} + +// Config 配置结构 +type Config struct { + SourceDir string // 源目录,如果是当前目录,设置为 "." + TargetDir string // 移动或复制到的目标目录,不建议设置为 ".",因为那不是脱了裤子放屁吗 + DarkThreshold float64 // 黑暗程度评分(0-1,越高越暗) + MinWidth int // 图片最小宽度 + MinHeight int // 图片最大宽度 + MinSizeKB int64 // 文件大小最小值 + MaxSizeMB int64 // 文件大小最大值 + CopyMode bool // true=复制, false=移动 + Parallel int // 并发数 + Verbose bool // 是否打印日志 +} + +// 默认配置 +func DefaultConfig() Config { + return Config{ + SourceDir: ".", + TargetDir: "./dark_wallpapers", + DarkThreshold: 0.75, + MinWidth: 1920, + MinHeight: 1080, + MinSizeKB: 100, // 最小100KB + MaxSizeMB: 100, // 最大100MB + CopyMode: false, // 默认移动 + Parallel: 24, // i5-12490f配置 + Verbose: true, + } +} + +// 处理单个图片文件 +func processImageFile(path string, config Config) (*WallpaperInfo, error) { + // 检查文件大小 + fileInfo, err := os.Stat(path) + if err != nil { + return nil, err + } + + sizeKB := fileInfo.Size() / 1024 + if sizeKB < config.MinSizeKB { + return nil, fmt.Errorf("文件太小: %dKB < %dKB", sizeKB, config.MinSizeKB) + } + + if fileInfo.Size() > config.MaxSizeMB*1024*1024 { + return nil, fmt.Errorf("文件太大: %dMB > %dMB", fileInfo.Size()/1024/1024, config.MaxSizeMB) + } + + // 打开图片文件 + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + // 解码图片 + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + + // 分析图片 + darkScore, avgColor, width, height := analyzeImage(img) + + // 检查分辨率 + if width < config.MinWidth || height < config.MinHeight { + return nil, fmt.Errorf("分辨率不足: %dx%d < %dx%d", width, height, config.MinWidth, config.MinHeight) + } + + // 检查黑暗程度 + if darkScore < config.DarkThreshold { + return nil, fmt.Errorf("不够黑暗: %.2f < %.2f", darkScore, config.DarkThreshold) + } + + return &WallpaperInfo{ + Path: path, + DarkScore: darkScore, + AvgColor: avgColor, + Size: fileInfo.Size(), + Width: width, + Height: height, + }, nil +} + +// 移动或复制文件到目标目录 +func moveOrCopyFile(wp *WallpaperInfo, config Config) error { + // 创建目标目录 + err := os.MkdirAll(config.TargetDir, 0755) + if err != nil { + return err + } + + // 生成目标文件名(保留原文件名,添加黑暗评分) + ext := filepath.Ext(wp.Path) + baseName := strings.TrimSuffix(filepath.Base(wp.Path), ext) + + // 清理文件名中的特殊字符 + cleanName := cleanFileName(baseName) + + // 添加分辨率信息 + newFilename := fmt.Sprintf("%s_[%dx%d]_dark%.0f%s", + cleanName, wp.Width, wp.Height, wp.DarkScore*100, ext) + dstPath := filepath.Join(config.TargetDir, newFilename) + + // 检查目标文件是否已存在 + if _, err := os.Stat(dstPath); err == nil { + // 文件已存在,添加序号 + for i := 1; i < 100; i++ { + newFilename = fmt.Sprintf("%s_[%dx%d]_dark%.0f_(%d)%s", + cleanName, wp.Width, wp.Height, wp.DarkScore*100, i, ext) + dstPath = filepath.Join(config.TargetDir, newFilename) + if _, err := os.Stat(dstPath); os.IsNotExist(err) { + break + } + } + } + + if config.CopyMode { + // 复制文件 + return copyFile(wp.Path, dstPath) + } else { + // 移动文件 + return moveFile(wp.Path, dstPath) + } +} + +// 清理文件名 +func cleanFileName(name string) string { + // 替换或移除特殊字符 + replacements := map[string]string{ + " ": "_", "/": "_", "\\": "_", ":": "_", + "*": "", "?": "", "\"": "", "<": "", ">": "", "|": "", + } + + result := name + for old, n := range replacements { + result = strings.ReplaceAll(result, old, n) + } + + // 限制长度 + if len(result) > 100 { + result = result[:100] + } + + return result +} + +// 复制文件 +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + _, err = dstFile.ReadFrom(srcFile) + return err +} + +// 移动文件 +func moveFile(src, dst string) error { + // 先尝试重命名(如果在同一分区) + err := os.Rename(src, dst) + if err == nil { + return nil + } + + // 如果重命名失败,则复制后删除 + if err := copyFile(src, dst); err != nil { + return err + } + + // 删除原文件 + return os.Remove(src) +} + +// 显示结果摘要 +func printSummary(wallpapers []WallpaperInfo, config Config) { + if len(wallpapers) == 0 { + fmt.Println("❌ 未找到符合条件的黑色壁纸") + return + } + + fmt.Println("\n🎯 找到的黑色调壁纸:") + fmt.Println(strings.Repeat("=", 60)) + + for i, wp := range wallpapers { + if i >= 20 { // 最多显示20个 + fmt.Printf("... 还有 %d 个未显示\n", len(wallpapers)-20) + break + } + + action := "移动到" + if config.CopyMode { + action = "复制到" + } + + fmt.Printf("%2d. %s\n", i+1, filepath.Base(wp.Path)) + fmt.Printf(" 原路径: %s\n", wp.Path) + fmt.Printf(" %s: %s\n", action, config.TargetDir) + fmt.Printf(" 黑暗程度: %.1f%%\n", wp.DarkScore*100) + fmt.Printf(" 平均颜色: RGB(%3d, %3d, %3d)\n", wp.AvgColor.R, wp.AvgColor.G, wp.AvgColor.B) + fmt.Printf(" 分辨率: %d × %d\n", wp.Width, wp.Height) + fmt.Printf(" 文件大小: %.1f MB\n", float64(wp.Size)/1024/1024) + + // 黑暗程度可视化 + bars := int(wp.DarkScore * 20) + fmt.Printf(" [") + for j := 0; j < 20; j++ { + if j < bars { + fmt.Printf("█") + } else { + fmt.Printf(" ") + } + } + fmt.Printf("]\n\n") + } +} + +// 保存结果到日志文件 +func saveResultsToLog(wallpapers []WallpaperInfo, config Config) { + logFile := filepath.Join(config.TargetDir, "dark_wallpapers_log.txt") + file, err := os.Create(logFile) + if err != nil { + log.Printf("无法创建日志文件: %v", err) + return + } + defer file.Close() + + fmt.Fprintf(file, "黑色壁纸处理日志 - %s\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Fprintf(file, "源目录: %s\n", config.SourceDir) + fmt.Fprintf(file, "目标目录: %s\n", config.TargetDir) + fmt.Fprintf(file, "黑暗阈值: %.2f\n", config.DarkThreshold) + fmt.Fprintf(file, "找到壁纸数量: %d\n\n", len(wallpapers)) + + for i, wp := range wallpapers { + fmt.Fprintf(file, "%d. %s\n", i+1, filepath.Base(wp.Path)) + fmt.Fprintf(file, " 黑暗程度: %.1f%%\n", wp.DarkScore*100) + fmt.Fprintf(file, " 分辨率: %dx%d\n", wp.Width, wp.Height) + fmt.Fprintf(file, " 大小: %.2f MB\n", float64(wp.Size)/1024/1024) + fmt.Fprintf(file, " 原路径: %s\n", wp.Path) + fmt.Fprintf(file, " 目标: %s/%s\n\n", config.TargetDir, filepath.Base(wp.Path)) + } + + fmt.Printf("📝 详细日志已保存到: %s\n", logFile) +} + +// 分析图片的黑暗程度和尺寸 +func analyzeImage(img image.Image) (darkScore float64, avgColor color.RGBA, width, height int) { + bounds := img.Bounds() + width = bounds.Dx() + height = bounds.Dy() + totalPixels := width * height + + if totalPixels == 0 { + return 0, color.RGBA{0, 0, 0, 255}, width, height + } + + var sumR, sumG, sumB uint64 + + // 采样分析(对于大图片进行采样以提高性能) + sampleStep := 1 + if totalPixels > 1000000 { // 超过100万像素则采样 + sampleStep = int(math.Sqrt(float64(totalPixels) / 100000)) + } + + sampleCount := 0 + for y := bounds.Min.Y; y < bounds.Max.Y; y += sampleStep { + for x := bounds.Min.X; x < bounds.Max.X; x += sampleStep { + r, g, b, _ := img.At(x, y).RGBA() + sumR += uint64(r >> 8) + sumG += uint64(g >> 8) + sumB += uint64(b >> 8) + sampleCount++ + } + } + + if sampleCount == 0 { + return 0, color.RGBA{0, 0, 0, 255}, width, height + } + + avgR := uint8(sumR / uint64(sampleCount)) + avgG := uint8(sumG / uint64(sampleCount)) + avgB := uint8(sumB / uint64(sampleCount)) + + // 计算黑暗程度评分 + brightness := 0.299*float64(avgR) + 0.587*float64(avgG) + 0.114*float64(avgB) + darkScore = 1.0 - (brightness / 255.0) + + // 增强黑色权重 + darkScore = math.Pow(darkScore, 0.6) + + // 如果平均颜色值很低,进一步提高评分 + if avgR < 40 && avgG < 40 && avgB < 40 { + darkScore = math.Min(1.0, darkScore*1.3) + } + + // 惩罚太鲜艳的颜色 + if avgR > 200 || avgG > 200 || avgB > 200 { + darkScore *= 0.7 + } + + return darkScore, color.RGBA{avgR, avgG, avgB, 255}, width, height +} + +// 查找并移动黑色壁纸 +func findAndMoveDarkWallpapers(config Config) ([]WallpaperInfo, error) { + var darkWallpapers []WallpaperInfo + var mu sync.Mutex + var wg sync.WaitGroup + + // 创建worker池 + jobs := make(chan string, 100) + results := make(chan *WallpaperInfo, 100) + errors := make(chan error, 100) + + // 启动worker + for i := 0; i < config.Parallel; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + for path := range jobs { + if config.Verbose { + fmt.Printf("Worker %d 处理: %s\n", workerID, filepath.Base(path)) + } + + wp, err := processImageFile(path, config) + if err != nil { + if config.Verbose { + fmt.Printf("跳过 %s: %v\n", filepath.Base(path), err) + } + continue + } + + results <- wp + } + }(i) + } + + // 收集结果并移动文件 + var processedCount int + var movedCount int + go func() { + for wp := range results { + mu.Lock() + darkWallpapers = append(darkWallpapers, *wp) + mu.Unlock() + + // 移动文件 + if err := moveOrCopyFile(wp, config); err != nil { + errors <- fmt.Errorf("移动文件失败 %s: %v", wp.Path, err) + continue + } + + movedCount++ + if config.Verbose { + fmt.Printf("✓ 已处理: %s (黑暗度: %.1f%%)\n", + filepath.Base(wp.Path), wp.DarkScore*100) + } + } + }() + + // 收集错误 + var processErrors []error + go func() { + for err := range errors { + processErrors = append(processErrors, err) + } + }() + + // 遍历源目录,发送任务 + err := filepath.WalkDir(config.SourceDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + + if d.IsDir() { + // 跳过目标目录 + if filepath.Clean(path) == filepath.Clean(config.TargetDir) { + return filepath.SkipDir + } + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + if supportedExts[ext] { + processedCount++ + jobs <- path + } + + return nil + }) + + close(jobs) + wg.Wait() + close(results) + close(errors) + + // 按黑暗程度排序 + sort.Slice(darkWallpapers, func(i, j int) bool { + return darkWallpapers[i].DarkScore > darkWallpapers[j].DarkScore + }) + + // 打印统计信息 + fmt.Printf("\n📊 处理完成统计:\n") + fmt.Printf(" 扫描文件总数: %d\n", processedCount) + fmt.Printf(" 找到黑色壁纸: %d\n", len(darkWallpapers)) + fmt.Printf(" 成功移动/复制: %d\n", movedCount) + if len(processErrors) > 0 { + fmt.Printf(" 错误数量: %d\n", len(processErrors)) + } + + if len(processErrors) > 0 && config.Verbose { + fmt.Println("\n⚠️ 错误列表:") + for _, err := range processErrors { + fmt.Printf(" %v\n", err) + } + } + + return darkWallpapers, err +} + +func main() { + // 配置 + config := DefaultConfig() + + // 可以在这里修改配置 + config.SourceDir = "." // 源目录,包含壁纸 + config.TargetDir = "./dark_wallpapers" // 目标目录 + config.DarkThreshold = 0.75 // 黑暗阈值 + config.MinWidth = 1920 + config.MinHeight = 1080 + config.CopyMode = false // false=移动,true=复制 + config.Verbose = true + + fmt.Println("🖤 黑色壁纸查找与移动工具") + fmt.Println(strings.Repeat("=", 40)) + fmt.Printf("源目录: %s\n", config.SourceDir) + fmt.Printf("目标目录: %s\n", config.TargetDir) + fmt.Printf("黑暗阈值: %.2f\n", config.DarkThreshold) + fmt.Printf("最小分辨率: %dx%d\n", config.MinWidth, config.MinHeight) + fmt.Printf("模式: %s\n", map[bool]string{true: "复制", false: "移动"}[config.CopyMode]) + fmt.Println() + + // 检查源目录是否存在 + if _, err := os.Stat(config.SourceDir); os.IsNotExist(err) { + log.Fatalf("源目录不存在: %s", config.SourceDir) + } + + // 执行查找和移动 + wallpapers, err := findAndMoveDarkWallpapers(config) + if err != nil { + log.Fatalf("处理失败: %v", err) + } + + // 显示结果 + printSummary(wallpapers, config) + + // 保存结果到日志文件 + if len(wallpapers) > 0 { + saveResultsToLog(wallpapers, config) + } +}