From 90ec28f91da6bc0420434f2150a99022ec906c1a Mon Sep 17 00:00:00 2001 From: Odboy Date: Wed, 10 Jul 2024 21:01:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E5=91=BD=E5=90=8D=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=B8=BA=20kenaito-wallpaper-organizer=20=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=98=8E=E6=9A=97/=E6=96=B9=E5=90=91/?= =?UTF-8?q?=E5=88=86=E8=BE=A8=E7=8E=87/UUID=20=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 144 +++++++++------------- build.sh | 4 +- go.mod | 2 +- main.go | 351 ++++++++++++++++++++++++++++++++++++------------------ 4 files changed, 290 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index ac9f692..ab6d219 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,29 @@ -# Kenaito Dark Wallpaper Finder 🖤 +# Kenaito Wallpaper Organizer 🖼️ -一个自动扫描、分析并整理暗色系壁纸的命令行工具。 - -程序会递归扫描指定目录下的所有图片,根据亮度算法计算每张图片的"黑暗程度"评分,然后按评分等级自动分类移动(或复制)到不同子目录中。 +一个自动扫描、分析并归类壁纸的命令行工具。它会递归扫描指定目录下的所有图片,分析每张图的**明暗程度**,并按**明暗等级 → 方向 → 分辨率**进行分类归档(移动或复制)。 ## 功能特性 -- **智能暗色分析**:基于 ITU-R BT.601 亮度公式计算黑暗评分,纯黑加分,鲜艳色惩罚 -- **多格式支持**:JPG、PNG、GIF、BMP、WebP、TIFF -- **五级分类**:按 ≥95%、≥85%、≥75%、≥65%、≥55% 五个黑暗等级自动归档 -- **并发处理**:默认 24 线程并行分析,大图自动采样加速 -- **灵活配置**:所有参数均可通过命令行调整 +- **智能明暗分析**:基于 ITU-R BT.601 亮度公式,同时统计暗色像素(亮度 < 80)与亮色像素(亮度 > 175)占比 +- **五级明暗分类**:暗色 `dark_95/85/75/65/55` 与亮色 `bright_95/85/75/65/55` 五档归档 +- **方向区分**:横屏 `horizontal` / 竖屏 `vertical` / 方形 `square` 子目录 +- **分辨率分档**:按长边区分 1K~8K 子目录 +- **特殊归置**:过小图(`fixed`)、分辨率不足(`other`)、异常文件(`error`) +- **多格式支持**:JPG、PNG、GIF、BMP、WebP、TIFF +- **并发处理**:默认 24 线程并行分析,大图自动采样加速 +- **UUID 命名**:归档后的文件统一重命名为随机 UUID,不保留原文件名 ## 编译 ```bash # Linux / macOS -go build -o kenaito-dark-wallpaper-finder main.go +go build -o kenaito-wallpaper-organizer main.go # Windows -go build -o kenaito-dark-wallpaper-finder.exe main.go +go build -o kenaito-wallpaper-organizer.exe main.go ``` -或使用构建脚本: +或使用构建脚本: ```bash bash build.sh @@ -33,26 +34,24 @@ bash build.sh ```bash # 在壁纸目录下运行(默认移动模式,等待5秒确认) cd ~/Pictures/wallpapers -./kenaito-dark-wallpaper-finder +./kenaito-wallpaper-organizer # 用复制模式试运行(不会移动原文件) -./kenaito-dark-wallpaper-finder -copy -sleep 0 +./kenaito-wallpaper-organizer -copy -sleep 0 ``` -执行后,源目录下会生成如下结构: +执行后,源目录下会生成如下结构: -``` -wallpapers/ ├── wallpapers_dark_wallpapers/ -│ ├── dark_95/ ← 黑暗度 ≥ 95% -│ │ ├── photo1_[2560x1440]_dark97.jpg -│ │ └── ... -│ ├── dark_85/ ← 黑暗度 ≥ 85% -│ ├── dark_75/ ← 黑暗度 ≥ 75% -│ ├── dark_65/ ← 黑暗度 ≥ 65% -│ └── dark_55/ ← 黑暗度 ≥ 55% -├── bright_wallpaper.jpg -└── ... +│ ├── dark_95/ +│ │ ├── horizontal/ → 1K/ 2K/ 4K/ … +│ │ ├── vertical/ → 1K/ 2K/ 4K/ … +│ │ └── square/ → 1K/ … +│ ├── bright_95/ # 与 dark_ 同构 +│ ├── neutral/ # 明暗皆不达标但尺寸合格 +│ ├── fixed/ # 长边 < 1080 的过小图 +│ ├── other/ # 尺寸不足 min-width/min-height +│ └── error/ # 大小超限或解码失败的异常文件 ``` ## 命令行参数 @@ -60,94 +59,61 @@ wallpapers/ | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `-source` | string | `.` | 源目录路径,递归扫描该目录下所有图片 | -| `-target` | string | 自动生成 | 目标目录基名,留空则自动使用 `<源目录名>_dark_wallpapers` | -| `-min-width` | int | `1920` | 图片最小宽度(像素),低于此值跳过 | -| `-min-height` | int | `1080` | 图片最小高度(像素),低于此值跳过 | -| `-min-size` | int | `100` | 最小文件大小(KB),小于此值跳过 | -| `-max-size` | int | `100` | 最大文件大小(MB),大于此值跳过(排除异常大文件) | +| `-target` | string | 自动生成 | 目标目录基名,留空自动使用 `<源目录名>_dark_wallpapers` | +| `-min-width` | int | `1080` | 图片最小宽度(像素),低于此值进入 other | +| `-min-height` | int | `720` | 图片最小高度(像素),低于此值进入 other | +| `-min-size` | int | `100` | 最小文件大小(KB),小于此值进入 error | +| `-max-size` | int | `100` | 最大文件大小(MB),大于此值进入 error | | `-copy` | bool | `false` | 复制模式,设为 true 则复制原文件而非移动 | -| `-parallel` | int | `24` | 并发分析线程数,根据 CPU 核心数调整 | +| `-parallel` | int | `24` | 并发分析线程数 | | `-verbose` | bool | `true` | 详细输出,显示每个文件的处理过程 | -| `-sleep` | int | `5` | 执行前等待秒数,设为 0 则立即执行。等待期间可按 Ctrl+C 取消 | +| `-sleep` | int | `5` | 执行前等待秒数,设为 0 则立即执行,等待期间可按 Ctrl+C 取消 | ## 使用示例 -### 基础用法 - ```bash -# 扫描当前目录,移动暗色壁纸到子目录(默认行为) -./kenaito-dark-wallpaper-finder - # 扫描指定目录 -./kenaito-dark-wallpaper-finder -source ~/Downloads/wallpapers +./kenaito-wallpaper-organizer -source ~/Downloads/wallpapers # 复制模式(保留原文件不动) -./kenaito-dark-wallpaper-finder -source ~/Pictures -copy +./kenaito-wallpaper-organizer -source ~/Pictures -copy # 跳过等待,直接执行 -./kenaito-dark-wallpaper-finder -sleep 0 -``` +./kenaito-wallpaper-organizer -sleep 0 -### 自定义筛选条件 - -```bash -# 只要 4K 壁纸(3840x2160 以上) -./kenaito-dark-wallpaper-finder -min-width 3840 -min-height 2160 - -# 只处理 10MB 以上的大图 -./kenaito-dark-wallpaper-finder -min-size 10240 - -# 只处理 500KB ~ 5MB 之间的图片 -./kenaito-dark-wallpaper-finder -min-size 500 -max-size 5 -``` - -### 性能调优 - -```bash -# 低配机器,减少并发数 -./kenaito-dark-wallpaper-finder -parallel 4 +# 只要 4K 以上壁纸 +./kenaito-wallpaper-organizer -min-width 3840 -min-height 2160 # 高配机器,关闭详细输出加速 -./kenaito-dark-wallpaper-finder -parallel 48 -verbose=false +./kenaito-wallpaper-organizer -parallel 48 -verbose=false ``` -### 组合使用 +## 分类优先级 -```bash -# 扫描外接硬盘上的壁纸,复制模式,4K 以上,不等待,静默执行 -./kenaito-dark-wallpaper-finder \ - -source /Volumes/ExternalHD/Wallpapers \ - -target ~/Pictures/DarkWallpapers \ - -copy \ - -min-width 3840 \ - -min-height 2160 \ - -sleep 0 \ - -verbose=false -``` +1. **fixed**:长边 < 1080 的过小图片 +2. **other**:尺寸达标(≥1080)但低于 `min-width/min-height` 阈值的图片 +3. **dark_N**(从高到低,每张只归入最高档) +4. **bright_N**(同理,处理完 dark 后剩余图中再判亮色档) +5. **neutral**:上面均未命中的图片 +6. **error**:文件大小超限或解码失败的异常文件 -## 工作原理 +所有正常分类(dark/bright/neutral/other)内部均按 方向 → 分辨率 分子目录。 -1. **扫描阶段**:递归遍历源目录,识别支持格式的图片文件,按分辨率和文件大小初步筛选 -2. **分析阶段**:多线程解码图片,对大图自动采样(>100万像素时按 sqrt(N/10万) 步长采样),通过 ITU-R BT.601 亮度公式计算黑暗评分 -3. **分类阶段**:按五个阈值等级(95%、85%、75%、65%、55%)筛选,移动到对应子目录。每个文件只归入其最高匹配的等级目录 - -### 黑暗评分算法 +## 明暗评分算法 ``` -brightness = 0.299×R + 0.587×G + 0.114×B -darkScore = (1 - brightness/255) ^ 0.6 +brightness = 0.299×R + 0.587×G + 0.114×B (ITU-R BT.601) +darkScore = 亮度 < 80 的像素占比 +lightScore = 亮度 > 175 的像素占比 ``` -- 纯黑像素(RGB < 40)额外加成 ×1.3 -- 鲜艳像素(任一通道 > 200)惩罚 ×0.7 - ## 注意事项 -- 默认使用**移动**模式,原文件会从源目录移走。首次使用建议加 `-copy` 试运行 -- 目标目录会被自动跳过,不会重复处理之前已整理的文件 -- 文件名冲突时自动追加序号 `_(1)`、`_(2)` 等 -- 支持的图片格式依赖 Go 标准库 + `golang.org/x/image`:JPG、PNG、GIF、BMP、WebP、TIFF +- 默认使用**移动**模式,原文件会从源目录移走,首次使用建议加 `-copy` 试运行 +- 目标目录会被自动跳过,不会重复处理之前已整理的文件 +- UUID 命名,极端情况下重名会自动追加 `_(1)`、`_(2)` 等序号 +- 支持的图片格式依赖 Go 标准库 + `golang.org/x/image`:JPG、PNG、GIF、BMP、WebP、TIFF ## License -MIT +MIT \ No newline at end of file diff --git a/build.sh b/build.sh index 4f2389b..1574050 100644 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ -rm -f kenaito-dark-wallpaper-finder.exe +rm -f kenaito-wallpaper-organizer.exe # build -go build -o kenaito-dark-wallpaper-finder.exe main.go +go build -o kenaito-wallpaper-organizer.exe main.go \ No newline at end of file diff --git a/go.mod b/go.mod index 729d663..767ac3f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module kenaito-media-group-tool +module kenaito-wallpaper-organizer go 1.21 diff --git a/main.go b/main.go index b52bc0d..c440c15 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/rand" "flag" "fmt" "image" @@ -32,12 +33,13 @@ var supportedExts = map[string]bool{ // WallpaperInfo 存储壁纸信息 type WallpaperInfo struct { - Path string - DarkScore float64 // 黑暗程度评分(0-1,越高越暗) - AvgColor color.RGBA - Size int64 - Width int - Height int + Path string + DarkScore float64 // 黑暗程度评分(0-1,越低越暗) + LightScore float64 // 亮度程度评分(0-1,越高越亮) + AvgColor color.RGBA + Size int64 + Width int + Height int } // Config 配置结构 @@ -58,8 +60,8 @@ type Config struct { func DefaultConfig() Config { return Config{ SourceDir: ".", - MinWidth: 1920, - MinHeight: 1080, + MinWidth: 1080, + MinHeight: 720, MinSizeKB: 100, MaxSizeMB: 100, CopyMode: false, @@ -102,7 +104,7 @@ func main() { } } - fmt.Println("🖤 黑色壁纸查找与移动工具") + fmt.Println("🖼️ Kenaito Wallpaper Organizer 壁纸整理工具") fmt.Println(strings.Repeat("=", 50)) fmt.Printf(" 源目录: %s\n", config.SourceDir) fmt.Printf(" 目标目录: %s(按黑暗等级分子目录)\n", config.TargetDir) @@ -126,57 +128,155 @@ func main() { log.Fatalf("扫描失败: %v", err) } - fmt.Printf("\n✅ 扫描完成!共 %d 张图片满足分辨率和大小要求\n", len(allWallpapers)) + fmt.Printf("\n✅ 扫描完成!共发现 %d 张图片(分辨率/大小不达标者将放入 other)\n", len(allWallpapers)) if len(allWallpapers) == 0 { fmt.Println("❌ 未找到符合条件的图片") return } - // 第二步:按黑暗等级分类并移动到不同子目录(从高到低,每张图片只归入一个等级) + // 第二步:分类并移动/复制,每张图片只归入一个目录 + // 优先级:分辨率不达标→ other;达标者按 暗色 → 亮色 → 中性 + + // 2.0 过小图片(长边<1080)放入独立的 fixed 目录;其余分辨率不达标的放入 other + conforming := make([]*WallpaperInfo, 0, len(allWallpapers)) + for i := range allWallpapers { + wp := &allWallpapers[i] + // 过小图片单独归置,不按明暗/分辨率细分 + longEdge := wp.Width + if wp.Height > longEdge { + longEdge = wp.Height + } + if longEdge < 1080 { + fixedDir := filepath.Join(config.TargetDir, "fixed") + fmt.Printf("[fixed] 长边 %d 过小 → fixed/\n", longEdge) + if err := moveOrCopyFile(wp, fixedDir, "", "", config.CopyMode); err != nil { + log.Printf("⚠️ 处理 fixed 失败 %s: %v", wp.Path, err) + } + continue + } + if wp.Width < config.MinWidth || wp.Height < config.MinHeight { + otherDir := filepath.Join(config.TargetDir, "other") + fmt.Printf("[other] 分辨率不足 %dx%d → other/\n", wp.Width, wp.Height) + if err := moveOrCopyFile(wp, otherDir, directionName(wp.Width, wp.Height), resolutionTierName(wp.Width, wp.Height), config.CopyMode); err != nil { + log.Printf("⚠️ 处理 other 失败 %s: %v", wp.Path, err) + } + continue + } + conforming = append(conforming, wp) + } + + // 第二步:暗色图片 → dark_N(从高到低,每张只归入最高档) for _, threshold := range thresholds { minScore := float64(threshold) / 100.0 targetDir := filepath.Join(config.TargetDir, fmt.Sprintf("dark_%d", threshold)) - - fmt.Printf("\n%s\n", strings.Repeat("=", 60)) - fmt.Printf("📁 处理黑暗等级 ≥ %d%% 的图片\n", threshold) - fmt.Printf(" 目标目录: %s\n", targetDir) - fmt.Println(strings.Repeat("=", 60)) - - filtered := filterByDarkness(allWallpapers, minScore) - - if len(filtered) == 0 { - fmt.Printf(" ⚠️ 没有符合黑暗等级 ≥ %d%% 的图片\n", threshold) - continue - } - - fmt.Printf(" 找到 %d 张符合条件的图片\n", len(filtered)) - - movedCount, moveErr := moveWallpapers(filtered, targetDir, config.CopyMode) - if moveErr != nil { - log.Printf("⚠️ 移动部分文件时出错: %v", moveErr) - } - - fmt.Printf(" ✓ 成功移动/复制 %d 张图片\n", movedCount) - - // 从待处理列表中移除本轮已处理的图片,避免后续低阈值重复处理 - processed := make(map[string]struct{}, len(filtered)) - for _, wp := range filtered { - processed[wp.Path] = struct{}{} - } - remaining := make([]WallpaperInfo, 0, len(allWallpapers)) - for _, wp := range allWallpapers { - if _, ok := processed[wp.Path]; !ok { + fmt.Printf("\n📁 处理暗色等级 ≥ %d%% 的图片\n", threshold) + moved := 0 + remaining := conforming[:0] + for _, wp := range conforming { + if wp.DarkScore >= minScore { + if err := moveOrCopyFile(wp, targetDir, directionName(wp.Width, wp.Height), resolutionTierName(wp.Width, wp.Height), config.CopyMode); err != nil { + log.Printf("⚠️ 移动失败 %s: %v", wp.Path, err) + } else { + moved++ + } + } else { remaining = append(remaining, wp) } } - allWallpapers = remaining + fmt.Printf(" ✅ 暗色 ≥ %d%%:移动 %d 张\n", threshold, moved) + conforming = remaining + } + + // 第三步:亮色图片 → bright_N(从高到低) + for _, threshold := range thresholds { + minScore := float64(threshold) / 100.0 + targetDir := filepath.Join(config.TargetDir, fmt.Sprintf("bright_%d", threshold)) + fmt.Printf("\n📁 处理亮色等级 ≥ %d%% 的图片\n", threshold) + moved := 0 + remaining := conforming[:0] + for _, wp := range conforming { + if wp.LightScore >= minScore { + if err := moveOrCopyFile(wp, targetDir, directionName(wp.Width, wp.Height), resolutionTierName(wp.Width, wp.Height), config.CopyMode); err != nil { + log.Printf("⚠️ 移动失败 %s: %v", wp.Path, err) + } else { + moved++ + } + } else { + remaining = append(remaining, wp) + } + } + fmt.Printf(" ✅ 亮色 ≥ %d%%:移动 %d 张\n", threshold, moved) + conforming = remaining + } + + // 第四步:剩余为中性图片 → neutral + moved := 0 + neutralDir := filepath.Join(config.TargetDir, "neutral") + for _, wp := range conforming { + if err := moveOrCopyFile(wp, neutralDir, directionName(wp.Width, wp.Height), resolutionTierName(wp.Width, wp.Height), config.CopyMode); err != nil { + log.Printf("⚠️ 移动失败 %s: %v", wp.Path, err) + } else { + moved++ + } + } + if moved > 0 { + fmt.Printf("\n📁 处理中性色图片 → neutral/:移动 %d 张\n", moved) } fmt.Printf("\n🎉 所有操作完成!文件已整理到: %s/\n", config.TargetDir) } -// analyzeImageFile 分析单个图片的黑暗程度(不做阈值过滤) +// SizeFilterError 表示文件大小不达标(min-size / max-size) +type SizeFilterError struct { + Path string + Reason string +} + +func (e *SizeFilterError) Error() string { return e.Reason } + +// DecodeError 表示文件无法打开或解码(损坏/非图片) +type DecodeError struct { + Path string + Reason string +} + +func (e *DecodeError) Error() string { return e.Reason } + +// moveToErrorDir 将处理异常的文件移动到 target/error 目录 +func moveToErrorDir(path string, config Config, err error) { + errDir := filepath.Join(config.TargetDir, "error") + if err := os.MkdirAll(errDir, 0755); err != nil { + log.Printf("⚠️ 创建 error 目录失败 %s: %v", errDir, err) + return + } + + ext := filepath.Ext(path) + dst := filepath.Join(errDir, newUUID()+ext) + // 文件名冲突:追加序号 + for i := 1; ; i++ { + if _, e := os.Stat(dst); os.IsNotExist(e) { + break + } + dst = filepath.Join(errDir, fmt.Sprintf("%s_(%d)%s", newUUID(), i, ext)) + } + + reason := "unknown" + switch err.(type) { + case *SizeFilterError: + reason = "size" + case *DecodeError: + reason = "decode" + } + + if mErr := moveFile(path, dst); mErr != nil { + log.Printf("⚠️ 移动文件到 error 目录失败 %s: %v", path, mErr) + return + } + fmt.Printf(" → error/%s: %s (%s)\n", reason, filepath.Base(dst), err.Error()) +} + +// analyzeImageFile 分析单个图片的黑暗/亮度程度(不做阈值过滤,返回完整信息) func analyzeImageFile(path string, config Config) (*WallpaperInfo, error) { fileInfo, err := os.Stat(path) if err != nil { @@ -185,54 +285,52 @@ func analyzeImageFile(path string, config Config) (*WallpaperInfo, error) { sizeKB := fileInfo.Size() / 1024 if sizeKB < config.MinSizeKB { - return nil, fmt.Errorf("文件太小: %dKB < %dKB", sizeKB, config.MinSizeKB) + return nil, &SizeFilterError{Path: path, Reason: fmt.Sprintf("文件太小: %dKB < %dKB", sizeKB, config.MinSizeKB)} } if fileInfo.Size() > config.MaxSizeMB*1024*1024 { - return nil, fmt.Errorf("文件太大: %dMB > %dMB", fileInfo.Size()/1024/1024, config.MaxSizeMB) + return nil, &SizeFilterError{Path: path, Reason: fmt.Sprintf("文件太大: %dMB > %dMB", fileInfo.Size()/1024/1024, config.MaxSizeMB)} } file, err := os.Open(path) if err != nil { - return nil, err + return nil, &DecodeError{Path: path, Reason: err.Error()} } defer file.Close() img, _, err := image.Decode(file) if err != nil { - return nil, err + return nil, &DecodeError{Path: path, Reason: err.Error()} } - 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) - } + darkScore, lightScore, avgColor, width, height := analyzeImage(img) return &WallpaperInfo{ - Path: path, - DarkScore: darkScore, - AvgColor: avgColor, - Size: fileInfo.Size(), - Width: width, - Height: height, + Path: path, + DarkScore: darkScore, + LightScore: lightScore, + AvgColor: avgColor, + Size: fileInfo.Size(), + Width: width, + Height: height, }, nil } -// analyzeImage 分析图片的黑暗程度和尺寸 -// 使用暗色像素占比(亮度 < 80 的像素比例)作为评分,比平均亮度更能准确反映暗色背景壁纸 -func analyzeImage(img image.Image) (darkScore float64, avgColor color.RGBA, width, height int) { +// analyzeImage 分析图片的黑暗程度、亮度程度和尺寸 +// darkScore: 暗色像素(亮度 < 80)占比;lightScore: 亮色像素(亮度 > 175)占比 +func analyzeImage(img image.Image) (darkScore float64, lightScore 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 + return 0, 0, color.RGBA{0, 0, 0, 255}, width, height } var sumR, sumG, sumB uint64 darkCount := 0 + lightCount := 0 sampleCount := 0 // 大图采样以提高性能 @@ -251,28 +349,29 @@ func analyzeImage(img image.Image) (darkScore float64, avgColor color.RGBA, widt sumG += uint64(g8) sumB += uint64(b8) - // 亮度计算(ITU-R BT.601),判断是否为暗色像素 + // 亮度计算(ITU-R BT.601) brightness := 0.299*float64(r8) + 0.587*float64(g8) + 0.114*float64(b8) if brightness < 80 { darkCount++ + } else if brightness > 175 { + lightCount++ } sampleCount++ } } if sampleCount == 0 { - return 0, color.RGBA{0, 0, 0, 255}, width, height + return 0, 0, color.RGBA{0, 0, 0, 255}, width, height } - // 暗色像素占比作为黑暗评分(0-1),比平均亮度更准确: - // 一张 80% 黑色背景 + 20% 明亮主体的壁纸,评分 0.80,正确反映"暗色背景"特性 darkScore = float64(darkCount) / float64(sampleCount) + lightScore = float64(lightCount) / float64(sampleCount) avgR := uint8(sumR / uint64(sampleCount)) avgG := uint8(sumG / uint64(sampleCount)) avgB := uint8(sumB / uint64(sampleCount)) - return darkScore, color.RGBA{avgR, avgG, avgB, 255}, width, height + return darkScore, lightScore, color.RGBA{avgR, avgG, avgB, 255}, width, height } // scanAllImages 一次性扫描所有图片并分析黑暗程度 @@ -299,8 +398,9 @@ func scanAllImages(config Config) ([]WallpaperInfo, error) { wp, err := analyzeImageFile(path, config) if err != nil { if config.Verbose { - fmt.Printf("跳过 %s: %v\n", filepath.Base(path), err) + fmt.Printf("⚠️ 处理异常 %s: %v -> error/\n", filepath.Base(path), err) } + moveToErrorDir(path, config, err) continue } results <- wp @@ -360,55 +460,86 @@ func scanAllImages(config Config) ([]WallpaperInfo, error) { return allWallpapers, err } -// filterByDarkness 根据黑暗阈值筛选图片 -func filterByDarkness(wallpapers []WallpaperInfo, minDarkScore float64) []WallpaperInfo { - var filtered []WallpaperInfo - for _, wp := range wallpapers { - if wp.DarkScore >= minDarkScore { - filtered = append(filtered, wp) +// resolutionTierName 根据图片较长边返回分辨率档位名称(如 "1K"、"4K"、"8K") +// 长边 < 1080 的过小图片单独归类为 "fixed" +func resolutionTierName(width, height int) string { + longEdge := width + if height > longEdge { + longEdge = height + } + // 过小图片单独归类 + if longEdge < 1080 { + return "fixed" + } + // 以标准横向分辨率作为各档位下限,取长边命中的最高档 + tiers := []struct { + name string + edge int + }{ + {"8K", 7680}, + {"7K", 7168}, + {"6K", 6144}, + {"5K", 5120}, + {"4K", 3840}, + {"3K", 3072}, + {"2K", 1920}, + {"1K", 1080}, + } + for _, t := range tiers { + if longEdge >= t.edge { + return t.name } } - return filtered + return "1K" } -// cleanFileName 清理文件名中的特殊字符 -func cleanFileName(name string) string { - replacements := map[string]string{ - " ": "_", "/": "_", "\\": "_", ":": "_", - "*": "", "?": "", "\"": "", "<": "", ">": "", "|": "", +// newUUID 通过 crypto/rand 生成的 32 位十六进制随机文件名(不含扩展名) +func newUUID() string { + const hexChars = "0123456789abcdef" + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + // 极难发生;回退到时间戳保证唯一 + return fmt.Sprintf("%x", time.Now().UnixNano()) } - result := name - for old, n := range replacements { - result = strings.ReplaceAll(result, old, n) + for i, b := range buf { + buf[i] = hexChars[b&0x0f] } - // 按 rune 截断,避免截断多字节 UTF-8 字符 - runes := []rune(result) - if len(runes) > 100 { - result = string(runes[:100]) - } - return result + return string(buf) } -// moveOrCopyFile 移动或复制文件到目标目录 -func moveOrCopyFile(wp *WallpaperInfo, targetDir string, copyMode bool) error { - if err := os.MkdirAll(targetDir, 0755); err != nil { +// directionName 返回图像的横竖方向(宽==高为 square,宽>高为 horizontal,否则 vertical) +func directionName(width, height int) string { + if width == height { + return "square" + } + if width > height { + return "horizontal" + } + return "vertical" +} + +// moveOrCopyFile 移动或复制文件到目标目录(direction 与 tier 为子目录,可为空) +func moveOrCopyFile(wp *WallpaperInfo, targetDir, direction, tier string, copyMode bool) error { + dir := targetDir + if direction != "" { + dir = filepath.Join(dir, direction) + } + if tier != "" { + dir = filepath.Join(dir, tier) + } + if err := os.MkdirAll(dir, 0755); 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(targetDir, newFilename) + newFilename := newUUID() + ext + dstPath := filepath.Join(dir, 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(targetDir, newFilename) + newFilename = fmt.Sprintf("%s_(%d)%s", newUUID(), i, ext) + dstPath = filepath.Join(dir, newFilename) if _, err := os.Stat(dstPath); os.IsNotExist(err) { break } @@ -449,21 +580,3 @@ func moveFile(src, dst string) error { } return os.Remove(src) } - -// moveWallpapers 将图片列表移动到指定目标目录 -func moveWallpapers(wallpapers []WallpaperInfo, targetDir string, copyMode bool) (int, error) { - movedCount := 0 - var firstErr error - - for _, wp := range wallpapers { - if err := moveOrCopyFile(&wp, targetDir, copyMode); err != nil { - if firstErr == nil { - firstErr = err - } - continue - } - movedCount++ - } - - return movedCount, firstErr -}