Skip to content

Commit 61a5e18

Browse files
committed
[重构与增强]: 优化哈希计算与图片工具集,移除 WebDAV 模块
- **功能重组**:将 `icon-maker` 升级为 `image-toolkit`,整合格式转换、ICNS/ICO 生成与解析功能,新增 `convert_img.py`、`dump_icns.py`、`dump_ico.py` - **哈希优化**:重写 `hash.py` 支持流式处理大文件,统一输入解析逻辑,提升性能与内存效率 - **目录树增强**:为 `tree.py` 增加文件过滤、排除模式支持,支持从文件读取忽略规则 - **依赖清理**:移除 `webdav` 模块及相关测试文件,简化项目结构 - **脚本调整**:优化 `activate_venv.sh` 避免自动安装依赖,仅保留虚拟环境激活提示 - **文档更新**:同步 README 说明,修正格式与依赖安装命令空格问题
1 parent 293c689 commit 61a5e18

File tree

12 files changed

+528
-317
lines changed

12 files changed

+528
-317
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
| **cli_logger** | loguru 日志配置示例,控制台 + 文件双通道输出 | [`cli_logger.py`](cli_logger/cli_logger.py) |
1313
| **dirwatch** | 实时监控文件夹变化(增/删/改/重命名) | [`dirwatch.py`](dirwatch/dirwatch.py) |
1414
| **hash** | 计算文件或文本的哈希值(MD5/SHA-1/SHA-2/SHA-3/BLAKE2/BLAKE3) | [`hash.py`](hash/hash.py) |
15-
| **icon-maker** | 一键生成 macOS / Windows 应用图标(`.icns` / `.ico` | [`make_icns.py`](icon-maker/make_icns.py) / [`make_ico.py`](icon-maker/make_ico.py) |
15+
| **image-toolkit** | 图片格式转换工具 + 一键生成/解析`.icns` / `.ico` | [`convert_img.py`](image-toolkit/convert_img.py) / [`dump_icns.py`](image-toolkit/dump_icns.py) / [`dump_ico.py`](image-toolkit/dump_ico.py) / [`make_icns.py`](image-toolkit/make_icns.py) / [`make_ico.py`](image-toolkit/make_ico.py) |
1616
| **m3u8_download** | m3u8 下载器,自动合并 ts 为单个视频 | [`m3u8_dl.py`](m3u8_download/m3u8_dl.py) |
1717
| **procmon** | 按进程名实时监控 CPU/内存/线程/句柄 | [`procmon.py`](procmon/procmon.py) |
1818
| **resolve** | 域名解析工具,快速获取 IP、端口、协议信息 | [`resolve.py`](resolve/resolve.py) |
1919
| **syncthing** | Syncthing API 封装,监控文件夹与设备状态 | [`syncthing_monitor.py`](syncthing/syncthing_monitor.py) |
2020
| **tree** | 可视化目录树生成工具 | [`tree.py`](tree/tree.py) |
2121
| **utils** | 通用工具库(颜色输出等) | [`colors.py`](utils/colors.py) |
22-
| **webdav** | 轻量级 WebDAV 客户端,支持上传/下载/删除/移动 | [`webdav.py`](webdav/webdav.py) |
2322
| **sync_req** | 依赖同步工具,从 pyproject.toml 生成 requirements.txt | [`sync_req.py`](sync_req.py) |
2423

2524
## 🚀 快速开始
@@ -39,7 +38,7 @@ source venv/bin/activate
3938
source activate_venv.sh
4039

4140
# 安装依赖
42-
pip install -e . -i https://pypi.tuna.tsinghua.edu.cn/simple
41+
pip install -e . -i https://pypi.tuna.tsinghua.edu.cn/simple
4342
```
4443

4544
### 同步依赖文件
@@ -51,7 +50,7 @@ pip install -e . -i https://pypi.tuna.tsinghua.edu.cn/simple
5150
python sync_req.py
5251

5352
# 使用生成的 requirements.txt 安装依赖
54-
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
53+
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
5554
```
5655

5756
### 使用说明

activate_venv.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,4 @@ source "$VENV_NAME/bin/activate"
2020
# 提示用户后续操作
2121
echo "虚拟环境已激活,当前 Python 路径: $(which python)"
2222

23-
current_path=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
24-
pip install --upgrade pip
25-
pip install -r "${current_path}/requirements.txt" -i https://pypi.tuna.tsinghua.edu.cn/simple
23+
echo "你可以使用 'deactivate' 命令退出虚拟环境"

hash/hash.py

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
import hashlib
1717
import os
1818
import sys
19-
from typing import Dict, List, Callable
19+
from typing import Dict, List, Callable, Union, IO
2020
from utils import Colors
2121

2222

23-
# ---------- 算法表 ----------
23+
# ---------------- 算法表 ----------------
2424
ALGO_MAP: Dict[str, Callable[[], "hashlib._Hash"]] = {}
2525

2626
# 标准库自带
@@ -49,33 +49,49 @@
4949
except ImportError:
5050
pass
5151

52-
53-
# ---------- 工具 ----------
54-
def calc_hash(data: bytes, algo: str) -> str:
55-
"""计算指定算法哈希值,返回十六进制字符串"""
56-
h = ALGO_MAP[algo]()
57-
h.update(data)
58-
return h.hexdigest()
59-
60-
61-
def read_target(target: str) -> bytes:
62-
"""从文件、文本或 stdin 读取二进制数据"""
63-
if target == "-":
52+
# ---------------- 流式核心 ----------------
53+
BUF = 1 << 20
54+
55+
56+
def multi_hash_stream(
57+
stream: Union[bytes, IO[bytes]], algos: List[str], buf_size: int = BUF
58+
) -> Dict[str, str]:
59+
"""一次读取,返回 algo->hex 的映射"""
60+
hashers = {a: ALGO_MAP[a]() for a in algos}
61+
if isinstance(stream, bytes): # 小文本
62+
for h in hashers.values():
63+
h.update(stream)
64+
return {a: h.hexdigest() for a, h in hashers.items()}
65+
# 文件 / stdin
66+
while chunk := stream.read(buf_size):
67+
for h in hashers.values():
68+
h.update(chunk)
69+
return {a: h.hexdigest() for a, h in hashers.items()}
70+
71+
72+
# ---------------- 输入解析 ----------------
73+
def parse_input(args) -> Union[bytes, IO[bytes]]:
74+
if args.target == "-":
6475
print(f"{Colors.OK}从标准输入读取数据{Colors.END}")
65-
return sys.stdin.buffer.read()
66-
if os.path.isfile(target):
67-
print(f"{Colors.OK}读取文件: {target}{Colors.END}")
68-
with open(target, "rb") as f:
69-
return f.read()
76+
return sys.stdin.buffer
77+
if args.text:
78+
print(f"{Colors.OK}按文本内容计算{Colors.END}")
79+
return args.target.encode("utf-8")
80+
if os.path.isfile(args.target):
81+
print(f"{Colors.OK}读取文件: {args.target}{Colors.END}")
82+
return open(args.target, "rb")
7083
print(f"{Colors.OK}按文本内容计算{Colors.END}")
71-
return target.encode("utf-8")
84+
return args.target.encode("utf-8")
7285

7386

74-
# ---------- 主入口 ----------
87+
# ---------------- 主入口 ----------------
7588
def main(argv: List[str] = None) -> None:
7689
parser = argparse.ArgumentParser(description="计算文件或文本的哈希值")
77-
parser.add_argument("target", nargs="?", help="文件路径、文本或 '-' 表示 stdin")
90+
parser.add_argument("target", nargs="?", help="文件路径、文本或 '-'")
7891
parser.add_argument("-a", "--algo", choices=list(ALGO_MAP), help="仅输出指定算法")
92+
parser.add_argument(
93+
"-t", "--text", action="store_true", help="强制将 TARGET 视为文本"
94+
)
7995
parser.add_argument("-l", "--list", action="store_true", help="列出支持的算法")
8096
args = parser.parse_args(argv)
8197

@@ -88,22 +104,32 @@ def main(argv: List[str] = None) -> None:
88104
return
89105

90106
if args.target is None:
91-
parser.error("缺少参数 target(或改用 -l 查看算法列表)")
107+
parser.error("缺少参数 TARGET(或改用 -l 查看算法列表)")
92108

93109
try:
94-
data = read_target(args.target)
110+
stream = parse_input(args)
95111
except OSError as e:
96-
print(f"{Colors.ERR}读取失败: {e}{Colors.END}", file=sys.stderr)
112+
print(f"{Colors.ERR}打开失败: {e}{Colors.END}", file=sys.stderr)
97113
sys.exit(2)
98114

99-
if args.algo:
100-
print(
101-
f"{Colors.BOLD}{args.algo.upper()}{Colors.END}: {calc_hash(data, args.algo)}"
102-
)
103-
else:
115+
# 要算哪些算法
116+
to_calc = [args.algo] if args.algo else sorted(ALGO_MAP)
117+
118+
# 统一走 multi_hash_stream
119+
if hasattr(stream, "seek"):
120+
stream.seek(0)
121+
results = multi_hash_stream(stream, to_calc)
122+
123+
# 输出
124+
if args.algo: # 单算法
125+
print(f"{Colors.BOLD}{args.algo.upper()}{Colors.END}: " f"{results[args.algo]}")
126+
else: # 多算法
104127
print(f"{Colors.BOLD}哈希值:{Colors.END}")
105128
for k in sorted(ALGO_MAP):
106-
print(f" {k.upper()}: {calc_hash(data, k)}")
129+
print(f" {k.upper()}: {results[k]}")
130+
131+
if hasattr(stream, "close"):
132+
stream.close()
107133

108134

109135
if __name__ == "__main__":

image-toolkit/convert_img.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
convert_img.py
4+
通用图片格式/分辨率批量转换工具
5+
6+
用法:
7+
./convert_img.py input.png
8+
./convert_img.py input.png -f webp -r 800x600
9+
./convert_img.py input.png -r 1920x1080 --no-ratio -q 90
10+
./convert_img.py --list-input
11+
./convert_img.py --list-output
12+
"""
13+
import os
14+
import sys
15+
import datetime
16+
import argparse
17+
from PIL import Image, ImageOps
18+
19+
20+
# -------------------- 列表格式 --------------------
21+
def list_input_formats():
22+
"""Pillow 能读取的格式"""
23+
return sorted(
24+
{
25+
ext.lstrip(".").lower()
26+
for ext, fmt in Image.registered_extensions().items()
27+
if fmt in Image.OPEN
28+
}
29+
)
30+
31+
32+
def list_output_formats():
33+
"""Pillow 能写入的格式"""
34+
return sorted(
35+
{
36+
ext.lstrip(".").lower()
37+
for ext, fmt in Image.registered_extensions().items()
38+
if fmt in Image.SAVE
39+
}
40+
)
41+
42+
43+
# -------------------- 分辨率解析 --------------------
44+
def parse_resolution(s: str):
45+
try:
46+
w, h = map(int, s.lower().split("x"))
47+
return w, h
48+
except Exception:
49+
raise argparse.ArgumentTypeError("分辨率格式必须是 宽x高,例如 800x600")
50+
51+
52+
# -------------------- 核心转换 --------------------
53+
def convert_image(
54+
src_path, dst_template: str, dst_size=None, keep_ratio=True, quality=95
55+
):
56+
"""
57+
:param dst_template: 带 {} 占位符的路径模板,例如
58+
'/tmp/demo_20250920_{}x{}.webp'
59+
"""
60+
with Image.open(src_path) as im:
61+
im = ImageOps.exif_transpose(im).convert("RGBA")
62+
63+
# 1. 缩放并拿到真实分辨率
64+
if dst_size:
65+
src_w, src_h = im.size
66+
dst_w, dst_h = dst_size
67+
if keep_ratio:
68+
ratio = min(dst_w / src_w, dst_h / src_h)
69+
dst_w, dst_h = int(src_w * ratio), int(src_h * ratio)
70+
im = im.resize((dst_w, dst_h), Image.LANCZOS)
71+
else:
72+
dst_w, dst_h = im.size # 未指定分辨率就用原图尺寸
73+
74+
# 2. 生成最终路径
75+
dst_path = dst_template.format(dst_w, dst_h)
76+
77+
# 3. 保存
78+
save_kw = {}
79+
ext = os.path.splitext(dst_path)[1].lower()
80+
if ext in (".jpg", ".jpeg"):
81+
im = im.convert("RGB")
82+
save_kw.update(quality=quality, optimize=True)
83+
elif ext == ".webp":
84+
save_kw.update(quality=quality)
85+
elif ext == ".png":
86+
save_kw.update(optimize=True)
87+
88+
im.save(dst_path, **save_kw)
89+
print(f"✅ 已生成 -> {dst_path}")
90+
91+
92+
# -------------------- 命令行入口 --------------------
93+
def main(argv=None):
94+
parser = argparse.ArgumentParser(description="万能图片转换小工具")
95+
parser.add_argument("input", nargs="?", help="输入图片路径")
96+
parser.add_argument(
97+
"-f", "--format", help="输出格式(jpg/png/bmp/webp/...),默认保持原格式"
98+
)
99+
parser.add_argument(
100+
"-r",
101+
"--resolution",
102+
type=parse_resolution,
103+
help="目标分辨率 宽x高,例如 800x600",
104+
)
105+
parser.add_argument(
106+
"--no-ratio", action="store_true", help="不保持宽高比(默认保持)"
107+
)
108+
parser.add_argument("-o", "--output", help="输出目录(默认跟输入图片同目录)")
109+
parser.add_argument(
110+
"-q", "--quality", type=int, default=95, help="JPEG/WebP 质量,默认 95"
111+
)
112+
group = parser.add_mutually_exclusive_group()
113+
group.add_argument(
114+
"--list-input", action="store_true", help="列出本脚本支持的所有输入格式"
115+
)
116+
group.add_argument(
117+
"--list-output", action="store_true", help="列出本脚本支持的所有输出格式"
118+
)
119+
120+
args = parser.parse_args(argv)
121+
122+
# ---- 列表模式 ----
123+
if args.list_input:
124+
print("本脚本支持的输入格式:")
125+
print(" ".join(list_input_formats()))
126+
return
127+
if args.list_output:
128+
print("本脚本支持的输出格式:")
129+
print(" ".join(list_output_formats()))
130+
return
131+
132+
# ---- 转换模式 ----
133+
if not args.input:
134+
parser.print_help()
135+
sys.exit("请提供输入文件,或使用 --list-input / --list-output 查看支持格式")
136+
if not os.path.isfile(args.input):
137+
sys.exit("输入文件不存在")
138+
139+
out_dir = args.output or os.path.dirname(os.path.abspath(args.input))
140+
os.makedirs(out_dir, exist_ok=True)
141+
142+
src_ext = os.path.splitext(args.input)[1][1:].lower()
143+
dst_ext = (args.format or src_ext).lower().strip(".")
144+
if dst_ext == "jpeg":
145+
dst_ext = "jpg"
146+
147+
date_str = datetime.datetime.now().strftime("%Y%m%d")
148+
dst_template = os.path.join(
149+
out_dir,
150+
f"{os.path.splitext(os.path.basename(args.input))[0]}_{date_str}_{{}}x{{}}.{dst_ext}",
151+
)
152+
153+
convert_image(
154+
args.input,
155+
dst_template,
156+
dst_size=args.resolution,
157+
keep_ratio=not args.no_ratio,
158+
quality=args.quality,
159+
)
160+
161+
162+
if __name__ == "__main__":
163+
main()

0 commit comments

Comments
 (0)