deepclean-mydisk 0.0.0-stage → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +354 -2
- package/assets/icon-180.png +0 -0
- package/cordis.patch.yml +5 -0
- package/docs/plugin/DETAIL.en.md +58 -0
- package/docs/plugin/DETAIL.zh.md +58 -0
- package/docs/plugin/shot-clean.jpg +0 -0
- package/docs/plugin/shot-home.jpg +0 -0
- package/docs/plugin/shot-move.jpg +0 -0
- package/docs/plugin/shot-space.jpg +0 -0
- package/lib/clean.js +537 -0
- package/lib/client.js +2625 -0
- package/lib/index.js +1944 -0
- package/lib/platform.js +29 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +67 -4
- package/resources/LICENSE-phosphor.txt +21 -0
- package/resources/images/disk.png +0 -0
- package/resources/rules.json +139 -0
- package/resources/scripts/deepclean.py +109 -0
- package/resources/scripts/photos-windows.ps1 +77 -0
- package/resources/scripts/photos.py +268 -0
- package/resources/scripts/platforms/windows.py +940 -0
- package/resources/scripts/scan.py +949 -0
- package/resources/scripts/tree.py +218 -0
package/lib/platform.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
// Protection is checked again on the host, independently of UI selection.
|
|
4
|
+
export function isProtected(target, whitelist, platform = process.platform) {
|
|
5
|
+
const paths = platform === 'win32' ? path.win32 : path.posix
|
|
6
|
+
if (typeof target !== 'string' || !paths.isAbsolute(target)) return false
|
|
7
|
+
return whitelist.some((root) => {
|
|
8
|
+
if (typeof root !== 'string' || !paths.isAbsolute(root)) return false
|
|
9
|
+
const contains = (parent, child) => {
|
|
10
|
+
const relative = paths.relative(paths.resolve(parent), paths.resolve(child))
|
|
11
|
+
return relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative))
|
|
12
|
+
}
|
|
13
|
+
// Removing an ancestor would also remove the protected descendant.
|
|
14
|
+
return contains(root, target) || contains(target, root)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseTreeOutput(out, platform = process.platform) {
|
|
19
|
+
const children = []
|
|
20
|
+
for (const line of out.split(/\r?\n/)) {
|
|
21
|
+
const idx = line.indexOf('\t')
|
|
22
|
+
if (idx < 1) continue
|
|
23
|
+
const size = Number(line.slice(0, idx))
|
|
24
|
+
const target = line.slice(idx + 1)
|
|
25
|
+
if (!Number.isFinite(size) || size < 0 || !target) continue
|
|
26
|
+
children.push({ path: target, kb: platform === 'win32' ? size / 1024 : size })
|
|
27
|
+
}
|
|
28
|
+
return children
|
|
29
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepclean-mydisk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Read-only disk audit and cleanup for macOS and Windows: caches, leftovers, similar photos, space breakdown, and cross-disk file moves.",
|
|
5
|
+
"author": "huangxiaoliang",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"disk",
|
|
10
|
+
"storage",
|
|
11
|
+
"cleanup",
|
|
12
|
+
"macos",
|
|
13
|
+
"windows"
|
|
14
|
+
],
|
|
15
|
+
"icon": "./assets/icon-180.png",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"resources",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"locale/*.json",
|
|
28
|
+
"docs/plugin",
|
|
29
|
+
"assets/icon-180.png"
|
|
30
|
+
],
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"platform": "web"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.3.0-0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.5",
|
|
45
|
+
"react": "^19.3.1",
|
|
46
|
+
"react-dom": "^19.3.1"
|
|
47
|
+
},
|
|
48
|
+
"exports": {
|
|
49
|
+
".": {
|
|
50
|
+
"default": "./lib/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./client": {
|
|
53
|
+
"default": "./lib/client.js"
|
|
54
|
+
},
|
|
55
|
+
"./package.json": "./package.json",
|
|
56
|
+
"./locale/*.json": "./locale/*.json"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"test": "node --test"
|
|
60
|
+
},
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/huangxiaoliang/deepclean-mydisk-plugin.git"
|
|
64
|
+
},
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/huangxiaoliang/deepclean-mydisk-plugin/issues"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/huangxiaoliang/deepclean-mydisk-plugin#readme"
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Phosphor Icons
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"description": "mac-disk-audit 分类规则。改这里不用改代码。",
|
|
4
|
+
"thresholds": {
|
|
5
|
+
"displayMinMb": 10,
|
|
6
|
+
"mediaConfirmMb": 50,
|
|
7
|
+
"commandMinMb": 100,
|
|
8
|
+
"bigFileMb": 300
|
|
9
|
+
},
|
|
10
|
+
"mediaHints": [
|
|
11
|
+
"video",
|
|
12
|
+
"player",
|
|
13
|
+
"tenvideo",
|
|
14
|
+
"iqiyi",
|
|
15
|
+
"youku",
|
|
16
|
+
"bilibili",
|
|
17
|
+
"tv.",
|
|
18
|
+
"music",
|
|
19
|
+
"netease",
|
|
20
|
+
"spotify",
|
|
21
|
+
"podcast",
|
|
22
|
+
"fm.",
|
|
23
|
+
"radio",
|
|
24
|
+
"movie",
|
|
25
|
+
"cinema",
|
|
26
|
+
"douyin",
|
|
27
|
+
"tiktok",
|
|
28
|
+
"kuaishou",
|
|
29
|
+
"xiaohongshu",
|
|
30
|
+
"zhihu",
|
|
31
|
+
"weibo",
|
|
32
|
+
"qqmusic"
|
|
33
|
+
],
|
|
34
|
+
"meetingHints": [
|
|
35
|
+
"meeting",
|
|
36
|
+
"zoom",
|
|
37
|
+
"teams",
|
|
38
|
+
"webex",
|
|
39
|
+
"voov",
|
|
40
|
+
"wemeet",
|
|
41
|
+
"wwmapp"
|
|
42
|
+
],
|
|
43
|
+
"userDataHints": [
|
|
44
|
+
"wechat",
|
|
45
|
+
"weixin",
|
|
46
|
+
"wxwork",
|
|
47
|
+
"wecom",
|
|
48
|
+
"feishu",
|
|
49
|
+
"lark",
|
|
50
|
+
"dingtalk",
|
|
51
|
+
"telegram",
|
|
52
|
+
"whatsapp",
|
|
53
|
+
"signal",
|
|
54
|
+
"messenger",
|
|
55
|
+
"qq.",
|
|
56
|
+
"qqexdoc",
|
|
57
|
+
"qqexbrowser",
|
|
58
|
+
"txdocs",
|
|
59
|
+
"notes",
|
|
60
|
+
"evernote",
|
|
61
|
+
"yinxiang",
|
|
62
|
+
"notion",
|
|
63
|
+
"docs",
|
|
64
|
+
"office",
|
|
65
|
+
"wps",
|
|
66
|
+
"pdf",
|
|
67
|
+
"mail",
|
|
68
|
+
"calendar",
|
|
69
|
+
"messages",
|
|
70
|
+
"photos",
|
|
71
|
+
"books",
|
|
72
|
+
"kindle",
|
|
73
|
+
"weread",
|
|
74
|
+
"portfolio",
|
|
75
|
+
"frodo",
|
|
76
|
+
"douban"
|
|
77
|
+
],
|
|
78
|
+
"devHints": [
|
|
79
|
+
"xcode",
|
|
80
|
+
"simulator",
|
|
81
|
+
"docker",
|
|
82
|
+
"electron",
|
|
83
|
+
"cursor",
|
|
84
|
+
"trae",
|
|
85
|
+
"codex"
|
|
86
|
+
],
|
|
87
|
+
"extSuffixRe": "(?i)\\.[A-Za-z0-9]*(widget|widgets|extension|ext|helper|share|shares|shareextension|notification|notificationservice|notificationserviceextension|notificationcontent|findersync|quicklook|quicklookpreview|quicklookthumbnail|intent|intents|intenthandler|intentui|intentsui|ddsintents|action|actionextension|siri|broadcast|replaykit|today|todaywidget|fileprovider|fileproviderextension|fileproviderui|xpcservice|launcher|recentfiles|menu|preview|thumbnail|ipchelper|bridgeservice|webapp|safariextension|sharingextension|serviceextension|widgetkit|docktileplugin|finderhelper)$",
|
|
88
|
+
"systemCacheNames": [
|
|
89
|
+
"CloudKit",
|
|
90
|
+
"GeoServices",
|
|
91
|
+
"com.apple.geod",
|
|
92
|
+
"com.apple.mediaanalysisd",
|
|
93
|
+
"com.apple.Safari",
|
|
94
|
+
"com.apple.QuickLook",
|
|
95
|
+
"com.apple.exchangesync"
|
|
96
|
+
],
|
|
97
|
+
"knownNames": {
|
|
98
|
+
"com.tencent.txdocs": "腾讯文档",
|
|
99
|
+
"com.tencent.qqexdoc": "腾讯文档组件",
|
|
100
|
+
"com.tencent.wwmapp": "企业微信会议组件",
|
|
101
|
+
"com.tencent.qqexbrowser": "QQ 浏览器组件",
|
|
102
|
+
"com.bytedance.douyin.desktop": "抖音桌面端",
|
|
103
|
+
"com.douban.frodo": "豆瓣",
|
|
104
|
+
"com.tencent.QQMusicMac": "QQ 音乐",
|
|
105
|
+
"com.netease.163music": "网易云音乐",
|
|
106
|
+
"com.tencent.weread": "微信读书",
|
|
107
|
+
"com.tencent.xinWeChat": "微信",
|
|
108
|
+
"com.tencent.qq": "QQ",
|
|
109
|
+
"com.bytedance.macos.feishu": "飞书",
|
|
110
|
+
"com.yinxiang.Mac": "印象笔记",
|
|
111
|
+
"com.tencent.tenvideo": "腾讯视频",
|
|
112
|
+
"com.xunlei.mvideo": "迅雷影音",
|
|
113
|
+
"tv.danmaku.bilianime": "哔哩哔哩",
|
|
114
|
+
"com.youku.mac": "优酷",
|
|
115
|
+
"com.iqiyi.player": "爱奇艺",
|
|
116
|
+
"com.tencent.meeting": "腾讯会议",
|
|
117
|
+
"com.tencent.portfolio": "腾讯微云",
|
|
118
|
+
"net.whatsapp.WhatsApp": "WhatsApp",
|
|
119
|
+
"com.tencent.yuanbao": "腾讯元宝",
|
|
120
|
+
"com.google.drivefs": "Google Drive",
|
|
121
|
+
"com.tencent.wework": "企业微信"
|
|
122
|
+
},
|
|
123
|
+
"denyPaths": [
|
|
124
|
+
"~/Library/Mail",
|
|
125
|
+
"~/Library/Messages",
|
|
126
|
+
"~/Library/Photos",
|
|
127
|
+
"~/Library/Keychains",
|
|
128
|
+
"~/Library/CloudStorage",
|
|
129
|
+
"~/Library/Group Containers",
|
|
130
|
+
"~/Library/Application Support"
|
|
131
|
+
],
|
|
132
|
+
"componentOf": {
|
|
133
|
+
"com.tencent.wwmapp": "com.tencent.WeWorkMac",
|
|
134
|
+
"com.tencent.wecom": "com.tencent.WeWorkMac",
|
|
135
|
+
"com.tencent.wework": "com.tencent.WeWorkMac",
|
|
136
|
+
"com.tencent.WeWork-Helper": "com.tencent.WeWorkMac",
|
|
137
|
+
"88L2Q4487U.com.tencent.WeWorkMac": "com.tencent.WeWorkMac"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""deepclean-mydisk 统一入口(v0.4.0)。
|
|
3
|
+
|
|
4
|
+
- macOS:调用 scripts/scan.py(成熟实现,行为与 v0.3.x 一致)
|
|
5
|
+
- Windows:调用 scripts/platforms/windows.py(只读,PowerShell 隔离区方案)
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
python deepclean.py # 自动识别平台
|
|
9
|
+
python deepclean.py --quick --redact # 透传给平台脚本
|
|
10
|
+
python deepclean.py --platform windows --self-test
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
VERSION = "0.5.0"
|
|
19
|
+
HERE = Path(__file__).resolve().parent
|
|
20
|
+
MACOS = HERE / "scan.py"
|
|
21
|
+
WINDOWS = HERE / "platforms" / "windows.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_platform():
|
|
25
|
+
if sys.platform.startswith("win"):
|
|
26
|
+
return "windows"
|
|
27
|
+
if sys.platform == "darwin":
|
|
28
|
+
return "macos"
|
|
29
|
+
return "macos"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def target(platform):
|
|
33
|
+
return WINDOWS if platform == "windows" else MACOS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def split_args(argv):
|
|
37
|
+
platform = None
|
|
38
|
+
self_test = False
|
|
39
|
+
rest = []
|
|
40
|
+
i = 0
|
|
41
|
+
while i < len(argv):
|
|
42
|
+
a = argv[i]
|
|
43
|
+
if a == "--platform":
|
|
44
|
+
platform = argv[i + 1] if i + 1 < len(argv) else None
|
|
45
|
+
i += 2
|
|
46
|
+
elif a.startswith("--platform="):
|
|
47
|
+
platform = a.split("=", 1)[1]
|
|
48
|
+
i += 1
|
|
49
|
+
elif a == "--self-test":
|
|
50
|
+
self_test = True
|
|
51
|
+
i += 1
|
|
52
|
+
else:
|
|
53
|
+
rest.append(a)
|
|
54
|
+
i += 1
|
|
55
|
+
return platform, self_test, rest
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def macos_self_test():
|
|
59
|
+
"""在临时 HOME 上跑一次 quick 扫描,验证只读链路与报告结构。"""
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix="deepclean-mac-test-") as td:
|
|
61
|
+
home = Path(td) / "home"
|
|
62
|
+
(home / "Library" / "Containers").mkdir(parents=True)
|
|
63
|
+
(home / "Library" / "Caches").mkdir(parents=True)
|
|
64
|
+
report = Path(td) / "report.md"
|
|
65
|
+
proc = subprocess.run(
|
|
66
|
+
[sys.executable, str(MACOS), "--quick", "--home", str(home), "--md", str(report)],
|
|
67
|
+
capture_output=True, text=True)
|
|
68
|
+
if proc.returncode != 0:
|
|
69
|
+
print("FAIL macos 扫描退出码", proc.returncode)
|
|
70
|
+
print(proc.stderr[-800:])
|
|
71
|
+
return 1
|
|
72
|
+
text = report.read_text(encoding="utf-8")
|
|
73
|
+
checks = [
|
|
74
|
+
("报告标题", "磁盘体检报告" in text),
|
|
75
|
+
("预计可释放", "预计可释放" in text),
|
|
76
|
+
("不建议清理", "不建议清理" in text),
|
|
77
|
+
("扫描范围", "扫描范围" in text),
|
|
78
|
+
]
|
|
79
|
+
ok = True
|
|
80
|
+
for name, cond in checks:
|
|
81
|
+
print(("PASS " if cond else "FAIL ") + name)
|
|
82
|
+
ok = ok and cond
|
|
83
|
+
print("\n自检结果:" + ("通过" if ok else "失败"))
|
|
84
|
+
return 0 if ok else 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
platform = None
|
|
89
|
+
if len(sys.argv) > 1:
|
|
90
|
+
platform, self_test, rest = split_args(sys.argv[1:])
|
|
91
|
+
else:
|
|
92
|
+
self_test, rest = False, []
|
|
93
|
+
if rest and rest[0] in ("--version", "-V"):
|
|
94
|
+
print(f"deepclean-mydisk {VERSION}")
|
|
95
|
+
return 0
|
|
96
|
+
platform = platform or detect_platform()
|
|
97
|
+
if platform not in ("macos", "windows"):
|
|
98
|
+
print(f"不支持的平台:{platform}", file=sys.stderr)
|
|
99
|
+
return 2
|
|
100
|
+
if self_test:
|
|
101
|
+
if platform == "windows":
|
|
102
|
+
return subprocess.call([sys.executable, str(WINDOWS), "--self-test"])
|
|
103
|
+
return macos_self_test()
|
|
104
|
+
cmd = [sys.executable, str(target(platform))] + rest
|
|
105
|
+
return subprocess.call(cmd)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
sys.exit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Read-only image decoding through the Windows .NET image codecs.
|
|
2
|
+
# Writes only explicitly requested temporary JPEG outputs (never input files).
|
|
3
|
+
param(
|
|
4
|
+
[Parameter(Mandatory=$true)][ValidateSet('hash','thumbnail')][string]$Mode,
|
|
5
|
+
[string]$InputPath,
|
|
6
|
+
[string]$OutputPath,
|
|
7
|
+
[ValidateRange(64,320)][int]$Size = 160
|
|
8
|
+
)
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
11
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
12
|
+
try {
|
|
13
|
+
Add-Type -AssemblyName System.Drawing
|
|
14
|
+
function Get-PhotoHash([string]$File) {
|
|
15
|
+
$image = $null; $bitmap = $null; $graphics = $null
|
|
16
|
+
try {
|
|
17
|
+
$image = [System.Drawing.Image]::FromFile($File)
|
|
18
|
+
$bitmap = [System.Drawing.Bitmap]::new(9, 8)
|
|
19
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
20
|
+
$graphics.DrawImage($image, 0, 0, 9, 8)
|
|
21
|
+
$hex = [System.Text.StringBuilder]::new()
|
|
22
|
+
$nibble = 0; $count = 0
|
|
23
|
+
for ($y = 0; $y -lt 8; $y++) {
|
|
24
|
+
for ($x = 0; $x -lt 8; $x++) {
|
|
25
|
+
$a = $bitmap.GetPixel($x, $y); $b = $bitmap.GetPixel($x + 1, $y)
|
|
26
|
+
$left = [Math]::Floor(0.299 * $a.R + 0.587 * $a.G + 0.114 * $a.B)
|
|
27
|
+
$right = [Math]::Floor(0.299 * $b.R + 0.587 * $b.G + 0.114 * $b.B)
|
|
28
|
+
$nibble = $nibble -shl 1
|
|
29
|
+
if ($left -gt $right) { $nibble = $nibble -bor 1 }
|
|
30
|
+
$count++
|
|
31
|
+
if ($count -eq 4) {
|
|
32
|
+
[void]$hex.Append($nibble.ToString('x'))
|
|
33
|
+
$nibble = 0; $count = 0
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return $hex.ToString()
|
|
38
|
+
} finally {
|
|
39
|
+
if ($graphics) { $graphics.Dispose() }
|
|
40
|
+
if ($bitmap) { $bitmap.Dispose() }
|
|
41
|
+
if ($image) { $image.Dispose() }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if ($Mode -eq 'hash') {
|
|
45
|
+
$files = ConvertFrom-Json -InputObject ([Console]::In.ReadToEnd())
|
|
46
|
+
$results = @(foreach ($file in $files) {
|
|
47
|
+
try { @{path=[string]$file; hash=(Get-PhotoHash ([string]$file))} }
|
|
48
|
+
catch { @{path=[string]$file; error=$_.Exception.Message} }
|
|
49
|
+
})
|
|
50
|
+
ConvertTo-Json -InputObject $results -Depth 4 -Compress
|
|
51
|
+
} else {
|
|
52
|
+
$job = ConvertFrom-Json -InputObject ([Console]::In.ReadToEnd())
|
|
53
|
+
$InputPath = [string]$job.path
|
|
54
|
+
$OutputPath = [string]$job.output
|
|
55
|
+
$Size = [int]$job.size
|
|
56
|
+
if ($Size -lt 64 -or $Size -gt 320) { throw 'Invalid thumbnail size' }
|
|
57
|
+
$image = $null; $bitmap = $null; $graphics = $null
|
|
58
|
+
try {
|
|
59
|
+
$image = [System.Drawing.Image]::FromFile($InputPath)
|
|
60
|
+
$scale = [Math]::Min(1.0, $Size / [double][Math]::Max($image.Width, $image.Height))
|
|
61
|
+
$width = [Math]::Max(1, [int]($image.Width * $scale))
|
|
62
|
+
$height = [Math]::Max(1, [int]($image.Height * $scale))
|
|
63
|
+
$bitmap = [System.Drawing.Bitmap]::new($width, $height)
|
|
64
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
65
|
+
$graphics.Clear([System.Drawing.Color]::White)
|
|
66
|
+
$graphics.DrawImage($image, 0, 0, $width, $height)
|
|
67
|
+
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Jpeg)
|
|
68
|
+
} finally {
|
|
69
|
+
if ($graphics) { $graphics.Dispose() }
|
|
70
|
+
if ($bitmap) { $bitmap.Dispose() }
|
|
71
|
+
if ($image) { $image.Dispose() }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
[Console]::Error.WriteLine($_.Exception.Message)
|
|
76
|
+
exit 1
|
|
77
|
+
}
|