feat: 功能改动
This commit is contained in:
@@ -5,11 +5,11 @@
|
||||
## 功能特性
|
||||
|
||||
- **智能明暗分析**:基于 ITU-R BT.601 亮度公式,同时统计暗色像素(亮度 < 80)与亮色像素(亮度 > 175)占比
|
||||
- **五级明暗分类**:暗色 `dark_95/85/75/65/55` 与亮色 `bright_95/85/75/65/55` 五档归档
|
||||
- **多级明暗分类**:暗色 `dark_100/95/85/75/65/55` 与亮色 `bright_85/75/65/55/45/35/25/15` 多档归档
|
||||
- **方向区分**:横屏 `horizontal` / 竖屏 `vertical` / 方形 `square` 子目录
|
||||
- **分辨率分档**:按长边区分 1K~8K 子目录
|
||||
- **特殊归置**:过小图(`fixed`)、分辨率不足(`other`)、异常文件(`error`)
|
||||
- **多格式支持**:JPG、PNG、GIF、BMP、WebP、TIFF
|
||||
- **多格式支持**:JPG/JPEG/JFIF/JPE、PNG/APNG、GIF、BMP/DIB、WebP、TIFF/TIF、PSD/PSB
|
||||
- **并发处理**:默认 24 线程并行分析,大图自动采样加速
|
||||
- **UUID 命名**:归档后的文件统一重命名为随机 UUID,不保留原文件名
|
||||
|
||||
@@ -43,12 +43,13 @@ cd ~/Pictures/wallpapers
|
||||
执行后,源目录下会生成如下结构:
|
||||
|
||||
├── wallpapers_wallpapers/
|
||||
│ ├── dark_95/
|
||||
│ ├── dark_100/
|
||||
│ │ ├── horizontal/ → 1K/ 2K/ 4K/ …
|
||||
│ │ ├── vertical/ → 1K/ 2K/ 4K/ …
|
||||
│ │ └── square/ → 1K/ …
|
||||
│ ├── bright_95/ # 与 dark_ 同构
|
||||
│ ├── neutral/ # 明暗皆不达标但尺寸合格
|
||||
│ ├── bright_85/ # 与 dark_ 同构(15~85)
|
||||
│ ├── psd/ # PSD/PSB,不做明暗分类
|
||||
│ ├── gif/ # GIF,不做明暗分类
|
||||
│ ├── fixed/ # 长边 < 1080 的过小图
|
||||
│ ├── other/ # 尺寸不足 min-width/min-height
|
||||
│ └── error/ # 大小超限或解码失败的异常文件
|
||||
@@ -90,14 +91,12 @@ cd ~/Pictures/wallpapers
|
||||
|
||||
## 分类优先级
|
||||
|
||||
1. **fixed**:长边 < 1080 的过小图片
|
||||
2. **other**:尺寸达标(≥1080)但低于 `min-width/min-height` 阈值的图片
|
||||
3. **dark_N**(从高到低,每张只归入最高档)
|
||||
4. **bright_N**(同理,处理完 dark 后剩余图中再判亮色档)
|
||||
5. **neutral**:上面均未命中的图片
|
||||
6. **error**:文件大小超限或解码失败的异常文件
|
||||
1. **psd/gif**:PSD/PSB 归 `psd/`,GIF 归 `gif/`,均不做明暗分类
|
||||
2. **fixed**:长边 < 1080 的过小图片
|
||||
3. **other**:尺寸达标(≥1080)但低于 `min-width/min-height` 阈值的图片
|
||||
4. **dark_N / bright_N**:达标图按主导分判定亮暗——`lightScore > darkScore` 归明亮、否则归暗色,每张图必归亮暗之一(无 neutral)
|
||||
|
||||
所有正常分类(dark/bright/neutral/other)内部均按 方向 → 分辨率 分子目录。
|
||||
所有正常分类(dark/bright/other)内部均按 方向 → 分辨率 分子目录。
|
||||
|
||||
## 明暗评分算法
|
||||
|
||||
@@ -112,7 +111,8 @@ lightScore = 亮度 > 175 的像素占比
|
||||
- 默认使用**移动**模式,原文件会从源目录移走,首次使用建议加 `-copy` 试运行
|
||||
- 目标目录会被自动跳过,不会重复处理之前已整理的文件
|
||||
- UUID 命名,极端情况下重名会自动追加 `_(1)`、`_(2)` 等序号
|
||||
- 支持的图片格式依赖 Go 标准库 + `golang.org/x/image`:JPG、PNG、GIF、BMP、WebP、TIFF
|
||||
- 支持的图片格式依赖 Go 标准库 + `golang.org/x/image` + `github.com/oov/psd`:JPG/JPEG/JFIF/JPE、PNG/APNG、GIF、BMP/DIB、WebP、TIFF/TIF、PSD/PSB
|
||||
- 无后缀名的图片会通过文件头(magic 字节)探测自动识别,不会被静默跳过
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -3,3 +3,8 @@ module kenaito-wallpaper-organizer
|
||||
go 1.21
|
||||
|
||||
require golang.org/x/image v0.16.0
|
||||
|
||||
require (
|
||||
github.com/gopherjs/gopherjs v1.21.0 // indirect
|
||||
github.com/oov/psd v0.0.0-20260122084234-c463b6a89e2f // indirect
|
||||
)
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
github.com/gopherjs/gopherjs v1.21.0 h1:5HEGrz+XhpCchubMGzuyLuGoCTlL/yCT7sGsT5Se/dw=
|
||||
github.com/gopherjs/gopherjs v1.21.0/go.mod h1:R2HIOen3IzYSzvmvkeD8WOfiLN9wueR/T5Y+6z326Ck=
|
||||
github.com/oov/psd v0.0.0-20260122084234-c463b6a89e2f h1:67UmABhaufAdSFaRO/wcaTn0XgHWCirjwfFf21ZNPAc=
|
||||
github.com/oov/psd v0.0.0-20260122084234-c463b6a89e2f/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
|
||||
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
|
||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||
|
||||
@@ -22,13 +22,17 @@ import (
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
_ "github.com/oov/psd"
|
||||
)
|
||||
|
||||
// supportedExts 支持的图片格式
|
||||
var supportedExts = map[string]bool{
|
||||
".jpg": true, ".jpeg": true, ".png": true,
|
||||
".bmp": true, ".webp": true, ".gif": true,
|
||||
".jpg": true, ".jpeg": true, ".jfif": true, ".jpe": true,
|
||||
".png": true, ".apng": true,
|
||||
".bmp": true, ".dib": true, ".webp": true, ".gif": true,
|
||||
".tiff": true, ".tif": true,
|
||||
".psd": true, ".psb": true,
|
||||
}
|
||||
|
||||
// WallpaperInfo 存储壁纸信息
|
||||
@@ -40,6 +44,7 @@ type WallpaperInfo struct {
|
||||
Size int64
|
||||
Width int
|
||||
Height int
|
||||
Ext string // 检测到的扩展名(无后缀图片用)
|
||||
}
|
||||
|
||||
// Config 配置结构
|
||||
@@ -62,7 +67,7 @@ func DefaultConfig() Config {
|
||||
SourceDir: ".",
|
||||
MinWidth: 1080,
|
||||
MinHeight: 720,
|
||||
MinSizeKB: 100,
|
||||
MinSizeKB: 50,
|
||||
MaxSizeMB: 100,
|
||||
CopyMode: false,
|
||||
Parallel: 24,
|
||||
@@ -119,7 +124,9 @@ func main() {
|
||||
}
|
||||
|
||||
// 黑暗阈值等级(从高到低)
|
||||
thresholds := []int{95, 85, 75, 65, 55}
|
||||
darkThresholds := []int{100, 95, 85, 75, 65, 55}
|
||||
// 亮色阈值等级(从高到低,15~85)
|
||||
brightThresholds := []int{85, 75, 65, 55, 45, 35, 25, 15}
|
||||
|
||||
// 第一步:一次性扫描所有图片并分析
|
||||
fmt.Println("\n🔍 开始扫描图片...")
|
||||
@@ -142,6 +149,15 @@ func main() {
|
||||
conforming := make([]*WallpaperInfo, 0, len(allWallpapers))
|
||||
for i := range allWallpapers {
|
||||
wp := &allWallpapers[i]
|
||||
// 0.5 PSD/GIF 文件单独归置,不按明暗分类
|
||||
if isPSDOrGIF(wp.Path) {
|
||||
dir := filepath.Join(config.TargetDir, psdGifDirName(wp.Path))
|
||||
fmt.Printf("[%s] %s → %s/\n", psdGifDirName(wp.Path), filepath.Base(wp.Path), psdGifDirName(wp.Path))
|
||||
if err := moveOrCopyFile(wp, dir, "", "", config.CopyMode); err != nil {
|
||||
log.Printf("⚠️ 处理 %s 失败 %s: %v", psdGifDirName(wp.Path), wp.Path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 过小图片单独归置,不按明暗/分辨率细分
|
||||
longEdge := wp.Width
|
||||
if wp.Height > longEdge {
|
||||
@@ -166,62 +182,51 @@ func main() {
|
||||
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📁 处理暗色等级 ≥ %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)
|
||||
}
|
||||
}
|
||||
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")
|
||||
// 第二步:按"主导分"判定亮/暗(不走多层筛选),每张图必归亮或暗之一
|
||||
darkSeconds, brightSeconds := 0, 0
|
||||
darkLowest := darkThresholds[len(darkThresholds)-1]
|
||||
brightLowest := brightThresholds[len(brightThresholds)-1]
|
||||
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)
|
||||
direction := directionName(wp.Width, wp.Height)
|
||||
tier := resolutionTierName(wp.Width, wp.Height)
|
||||
|
||||
if wp.LightScore > wp.DarkScore {
|
||||
// 亮色主导:取 lightScore 命中的最高一档,不足最低档则归最低档
|
||||
picked := brightLowest
|
||||
for _, t := range brightThresholds {
|
||||
if wp.LightScore >= float64(t)/100.0 {
|
||||
picked = t
|
||||
break
|
||||
}
|
||||
}
|
||||
targetDir := filepath.Join(config.TargetDir, fmt.Sprintf("bright_%d", picked))
|
||||
if err := moveOrCopyFile(wp, targetDir, direction, tier, config.CopyMode); err != nil {
|
||||
log.Printf("⚠️ 移动失败 %s: %v", wp.Path, err)
|
||||
} else {
|
||||
brightSeconds++
|
||||
}
|
||||
} else {
|
||||
moved++
|
||||
// 暗色主导(含两者皆 0 的中间灰)→ 命中最高 dark 档,不足归最低档
|
||||
picked := darkLowest
|
||||
for _, t := range darkThresholds {
|
||||
if wp.DarkScore >= float64(t)/100.0 {
|
||||
picked = t
|
||||
break
|
||||
}
|
||||
}
|
||||
targetDir := filepath.Join(config.TargetDir, fmt.Sprintf("dark_%d", picked))
|
||||
if err := moveOrCopyFile(wp, targetDir, direction, tier, config.CopyMode); err != nil {
|
||||
log.Printf("⚠️ 移动失败 %s: %v", wp.Path, err)
|
||||
} else {
|
||||
darkSeconds++
|
||||
}
|
||||
}
|
||||
}
|
||||
if moved > 0 {
|
||||
fmt.Printf("\n📁 处理中性色图片 → neutral/:移动 %d 张\n", moved)
|
||||
if brightSeconds > 0 {
|
||||
fmt.Printf("\n📁 亮色主导 → bright_N:移动 %d 张\n", brightSeconds)
|
||||
}
|
||||
if darkSeconds > 0 {
|
||||
fmt.Printf("\n📁 暗色主导 → dark_N:移动 %d 张\n", darkSeconds)
|
||||
}
|
||||
|
||||
fmt.Printf("\n🎉 所有操作完成!文件已整理到: %s/\n", config.TargetDir)
|
||||
@@ -298,7 +303,7 @@ func analyzeImageFile(path string, config Config) (*WallpaperInfo, error) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
img, imageName, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, &DecodeError{Path: path, Reason: err.Error()}
|
||||
}
|
||||
@@ -313,6 +318,7 @@ func analyzeImageFile(path string, config Config) (*WallpaperInfo, error) {
|
||||
Size: fileInfo.Size(),
|
||||
Width: width,
|
||||
Height: height,
|
||||
Ext: detectExtension(path, imageName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -437,7 +443,8 @@ func scanAllImages(config Config) ([]WallpaperInfo, error) {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if supportedExts[ext] {
|
||||
// 支持的后缀直接处理;无后缀文件按文件内容探测是否为图片(避免静默跳过)
|
||||
if supportedExts[ext] || (ext == "" && isImageByContent(path)) {
|
||||
processedCount++
|
||||
jobs <- path
|
||||
}
|
||||
@@ -507,6 +514,57 @@ func newUUID() string {
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// isPSDOrGIF 判断扩展名是否为 PSD/PSB/GIF(单独归置,不参与明暗分类)
|
||||
func isPSDOrGIF(path string) bool {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".psd", ".psb", ".gif":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// psdGifDirName 返回 PSD/GIF 对应的独立目录名(gif 用 gif,PSD/PSB 用 psd)
|
||||
func psdGifDirName(path string) string {
|
||||
if strings.ToLower(filepath.Ext(path)) == ".gif" {
|
||||
return "gif"
|
||||
}
|
||||
return "psd"
|
||||
}
|
||||
|
||||
// formatExtensions 格式名到扩展名的映射(image.Image.Decode 返回的格式名)
|
||||
var formatExtensions = map[string]string{
|
||||
"png": ".png",
|
||||
"jpeg": ".jpg",
|
||||
"gif": ".gif",
|
||||
"bmp": ".bmp",
|
||||
"tiff": ".tiff",
|
||||
"webp": ".webp",
|
||||
"psd": ".psd",
|
||||
"psb": ".psb",
|
||||
}
|
||||
|
||||
// detectExtension 决定归档用扩展名:优先保留原后缀(如 .jfif),无后缀时按解码出的格式名补一个
|
||||
func detectExtension(path, imageName string) string {
|
||||
if ext := strings.ToLower(filepath.Ext(path)); ext != "" {
|
||||
return ext
|
||||
}
|
||||
if ext, ok := formatExtensions[imageName]; ok {
|
||||
return ext
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isImageByContent 通过读取文件头(magic 字节)判断是否为已注册的图片格式,用于无后缀文件
|
||||
func isImageByContent(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
_, _, err = image.DecodeConfig(f)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// directionName 返回图像的横竖方向(宽==高为 square,宽>高为 horizontal,否则 vertical)
|
||||
func directionName(width, height int) string {
|
||||
if width == height {
|
||||
@@ -531,7 +589,10 @@ func moveOrCopyFile(wp *WallpaperInfo, targetDir, direction, tier string, copyMo
|
||||
return err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(wp.Path)
|
||||
ext := wp.Ext
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(wp.Path)
|
||||
}
|
||||
newFilename := newUUID() + ext
|
||||
dstPath := filepath.Join(dir, newFilename)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user