aegis-watermark 0.1.0__tar.gz
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.
- aegis_watermark-0.1.0/PKG-INFO +104 -0
- aegis_watermark-0.1.0/README.md +73 -0
- aegis_watermark-0.1.0/aegis/__init__.py +0 -0
- aegis_watermark-0.1.0/aegis/cli.py +241 -0
- aegis_watermark-0.1.0/aegis/core/__init__.py +0 -0
- aegis_watermark-0.1.0/aegis/core/deep.py +16 -0
- aegis_watermark-0.1.0/aegis/core/frequency.py +88 -0
- aegis_watermark-0.1.0/aegis/core/spatial.py +18 -0
- aegis_watermark-0.1.0/aegis/handlers/__init__.py +0 -0
- aegis_watermark-0.1.0/aegis/handlers/image.py +15 -0
- aegis_watermark-0.1.0/aegis/handlers/pdf.py +6 -0
- aegis_watermark-0.1.0/aegis/handlers/ppt.py +101 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/PKG-INFO +104 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/SOURCES.txt +18 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/dependency_links.txt +1 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/entry_points.txt +2 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/requires.txt +9 -0
- aegis_watermark-0.1.0/aegis_watermark.egg-info/top_level.txt +1 -0
- aegis_watermark-0.1.0/setup.cfg +4 -0
- aegis_watermark-0.1.0/setup.py +38 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegis-watermark
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A professional hidden watermark tool for images and PPTX documents.
|
|
5
|
+
Home-page: https://github.com/your-repo/Aegis-Watermark
|
|
6
|
+
Author: Aegis Team
|
|
7
|
+
Author-email: admin@aegis-watermark.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: blind-watermark
|
|
14
|
+
Requires-Dist: opencv-python
|
|
15
|
+
Requires-Dist: numpy<2.0.0
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: Pillow
|
|
18
|
+
Requires-Dist: pyfiglet
|
|
19
|
+
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: python-pptx
|
|
21
|
+
Requires-Dist: questionary
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# Aegis (神盾) - 专业级隐形水印保护工具
|
|
33
|
+
|
|
34
|
+
Aegis 是一个基于频域盲水印技术的版权保护工具。它能够为图像及 PPTX 文档嵌入肉眼不可见的“视觉指纹”,即使文件经过截图、压缩或格式转换,依然可以提取出清晰的版权证据。
|
|
35
|
+
|
|
36
|
+
## 🌟 核心亮点
|
|
37
|
+
|
|
38
|
+
- **视觉水印技术**: 不同于脆弱的文本编码,Aegis 嵌入的是视觉轮廓,对图像损伤具有极强的抗性。
|
|
39
|
+
- **SHA-256 安全加固**: 支持任意长度字符串密钥,通过哈希拉伸技术,彻底杜绝暴力破解和撞库。
|
|
40
|
+
- **交互式控制台**: 输入 `aegis` 即可进入专业感十足的中英双语交互式主菜单,无需记忆复杂指令。
|
|
41
|
+
- **全自动文档保护**: 针对 `.pptx` 格式提供深度加固,自动识别并保护文档内部的所有高价值图像素材。
|
|
42
|
+
- **极简专业 UI**: 借鉴现代 CLI 设计,提供 ASCII Banner、动态加载动画及结构化分析报告。
|
|
43
|
+
|
|
44
|
+
## 🛠️ 技术栈
|
|
45
|
+
|
|
46
|
+
- **核心驱动**: `blind-watermark` (频率域 DCT/DWT 变换)
|
|
47
|
+
- **界面引擎**: `Rich` (终端美化) & `questionary` (交互菜单)
|
|
48
|
+
- **图像处理**: `OpenCV`, `Pillow`, `NumPy`
|
|
49
|
+
- **文档处理**: `python-pptx`
|
|
50
|
+
|
|
51
|
+
## 🚀 快速开始
|
|
52
|
+
|
|
53
|
+
### 1. 环境准备
|
|
54
|
+
确保你的系统安装了 Python 3.8+。
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 克隆项目并进入目录
|
|
58
|
+
git clone https://github.com/your-repo/Aegis-Watermark.git
|
|
59
|
+
cd Aegis-Watermark
|
|
60
|
+
|
|
61
|
+
# 安装依赖
|
|
62
|
+
pip install -r requirements.txt
|
|
63
|
+
|
|
64
|
+
# 以开发模式安装
|
|
65
|
+
pip install -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. 运行工具
|
|
69
|
+
在终端输入命令即可启动:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
aegis
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. 使用模式
|
|
76
|
+
|
|
77
|
+
- **交互模式**: 直接输入 `aegis`,跟随菜单引导完成操作。
|
|
78
|
+
- **命令模式**: 支持静默调用,适合脚本集成:
|
|
79
|
+
- 嵌入:`aegis embed -i input.png -o output.png -t "ID:716" -k "MyPassword"`
|
|
80
|
+
- 提取:`aegis extract -i output.png -k "MyPassword"`
|
|
81
|
+
|
|
82
|
+
## 📂 目录结构
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
aegis/
|
|
86
|
+
├── cli.py # 交互式双语 CLI 入口
|
|
87
|
+
├── core/ # 核心算法 (视觉水印 & 哈希加固)
|
|
88
|
+
└── handlers/ # 格式适配 (Image & PPTX)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 🔒 安全建议
|
|
92
|
+
|
|
93
|
+
- **密钥设置**: 建议使用包含字母和数字的长字符串作为密钥。
|
|
94
|
+
- **默认机制**: 若不输入密钥,系统将默认使用密钥 "1"。
|
|
95
|
+
- **版权验证**: 提取出的结果图片若能清晰辨认文字轮廓,即具备版权铁证效力。
|
|
96
|
+
|
|
97
|
+
## 📝 路线图
|
|
98
|
+
|
|
99
|
+
- [ ] 支持 PDF 矢量图层水印
|
|
100
|
+
- [ ] 增加多线程批量处理模式
|
|
101
|
+
- [ ] 集成 AI 深度学习水印算法
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
**Aegis - 为每一份智力成果披上隐形神盾。**
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Aegis (神盾) - 专业级隐形水印保护工具
|
|
2
|
+
|
|
3
|
+
Aegis 是一个基于频域盲水印技术的版权保护工具。它能够为图像及 PPTX 文档嵌入肉眼不可见的“视觉指纹”,即使文件经过截图、压缩或格式转换,依然可以提取出清晰的版权证据。
|
|
4
|
+
|
|
5
|
+
## 🌟 核心亮点
|
|
6
|
+
|
|
7
|
+
- **视觉水印技术**: 不同于脆弱的文本编码,Aegis 嵌入的是视觉轮廓,对图像损伤具有极强的抗性。
|
|
8
|
+
- **SHA-256 安全加固**: 支持任意长度字符串密钥,通过哈希拉伸技术,彻底杜绝暴力破解和撞库。
|
|
9
|
+
- **交互式控制台**: 输入 `aegis` 即可进入专业感十足的中英双语交互式主菜单,无需记忆复杂指令。
|
|
10
|
+
- **全自动文档保护**: 针对 `.pptx` 格式提供深度加固,自动识别并保护文档内部的所有高价值图像素材。
|
|
11
|
+
- **极简专业 UI**: 借鉴现代 CLI 设计,提供 ASCII Banner、动态加载动画及结构化分析报告。
|
|
12
|
+
|
|
13
|
+
## 🛠️ 技术栈
|
|
14
|
+
|
|
15
|
+
- **核心驱动**: `blind-watermark` (频率域 DCT/DWT 变换)
|
|
16
|
+
- **界面引擎**: `Rich` (终端美化) & `questionary` (交互菜单)
|
|
17
|
+
- **图像处理**: `OpenCV`, `Pillow`, `NumPy`
|
|
18
|
+
- **文档处理**: `python-pptx`
|
|
19
|
+
|
|
20
|
+
## 🚀 快速开始
|
|
21
|
+
|
|
22
|
+
### 1. 环境准备
|
|
23
|
+
确保你的系统安装了 Python 3.8+。
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 克隆项目并进入目录
|
|
27
|
+
git clone https://github.com/your-repo/Aegis-Watermark.git
|
|
28
|
+
cd Aegis-Watermark
|
|
29
|
+
|
|
30
|
+
# 安装依赖
|
|
31
|
+
pip install -r requirements.txt
|
|
32
|
+
|
|
33
|
+
# 以开发模式安装
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. 运行工具
|
|
38
|
+
在终端输入命令即可启动:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
aegis
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. 使用模式
|
|
45
|
+
|
|
46
|
+
- **交互模式**: 直接输入 `aegis`,跟随菜单引导完成操作。
|
|
47
|
+
- **命令模式**: 支持静默调用,适合脚本集成:
|
|
48
|
+
- 嵌入:`aegis embed -i input.png -o output.png -t "ID:716" -k "MyPassword"`
|
|
49
|
+
- 提取:`aegis extract -i output.png -k "MyPassword"`
|
|
50
|
+
|
|
51
|
+
## 📂 目录结构
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
aegis/
|
|
55
|
+
├── cli.py # 交互式双语 CLI 入口
|
|
56
|
+
├── core/ # 核心算法 (视觉水印 & 哈希加固)
|
|
57
|
+
└── handlers/ # 格式适配 (Image & PPTX)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 🔒 安全建议
|
|
61
|
+
|
|
62
|
+
- **密钥设置**: 建议使用包含字母和数字的长字符串作为密钥。
|
|
63
|
+
- **默认机制**: 若不输入密钥,系统将默认使用密钥 "1"。
|
|
64
|
+
- **版权验证**: 提取出的结果图片若能清晰辨认文字轮廓,即具备版权铁证效力。
|
|
65
|
+
|
|
66
|
+
## 📝 路线图
|
|
67
|
+
|
|
68
|
+
- [ ] 支持 PDF 矢量图层水印
|
|
69
|
+
- [ ] 增加多线程批量处理模式
|
|
70
|
+
- [ ] 集成 AI 深度学习水印算法
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
**Aegis - 为每一份智力成果披上隐形神盾。**
|
|
File without changes
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import pyfiglet
|
|
4
|
+
import questionary
|
|
5
|
+
import hashlib
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.align import Align
|
|
11
|
+
from rich.rule import Rule
|
|
12
|
+
|
|
13
|
+
# 禁用 OpenCV 警告信息,保持界面纯净
|
|
14
|
+
os.environ["OPENCV_LOG_LEVEL"] = "OFF"
|
|
15
|
+
|
|
16
|
+
from aegis.handlers.ppt import PPTHandler
|
|
17
|
+
from aegis.handlers.image import ImageHandler
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
# --- 多语言配置 ---
|
|
22
|
+
MESSAGES = {
|
|
23
|
+
"zh": {
|
|
24
|
+
"title": "Aegis",
|
|
25
|
+
"menu_prompt": "主菜单",
|
|
26
|
+
"menu_embed": "嵌入水印 (Embed)",
|
|
27
|
+
"menu_extract": "提取分析 (Extract)",
|
|
28
|
+
"menu_exit": "退出程序 (Exit)",
|
|
29
|
+
"path_input": "输入文件路径",
|
|
30
|
+
"path_output": "保存结果为",
|
|
31
|
+
"watermark_text": "水印文本内容",
|
|
32
|
+
"key_prompt": "设置密钥 (不填则默认为 1)",
|
|
33
|
+
"processing": "处理中",
|
|
34
|
+
"scanning": "分析中",
|
|
35
|
+
"success_embed": "嵌入成功!",
|
|
36
|
+
"fail_embed": "嵌入失败。",
|
|
37
|
+
"report_title": "分析报告",
|
|
38
|
+
"col_item": "字段",
|
|
39
|
+
"col_info": "内容",
|
|
40
|
+
"target_file": "分析对象",
|
|
41
|
+
"key_fingerprint": "安全指纹",
|
|
42
|
+
"status": "提取状态",
|
|
43
|
+
"evidence": "结果路径",
|
|
44
|
+
"note_title": "核验提示",
|
|
45
|
+
"note_body": "请查看结果图片。若文字轮廓清晰,则版权校验通过。",
|
|
46
|
+
"fail_extract": "分析失败: 未发现有效水印。",
|
|
47
|
+
"error_file": "路径错误,请重新输入。"
|
|
48
|
+
},
|
|
49
|
+
"en": {
|
|
50
|
+
"title": "Aegis",
|
|
51
|
+
"menu_prompt": "Main Menu",
|
|
52
|
+
"menu_embed": "Embed Watermark",
|
|
53
|
+
"menu_extract": "Extract & Analyze",
|
|
54
|
+
"menu_exit": "Exit",
|
|
55
|
+
"path_input": "File Path",
|
|
56
|
+
"path_output": "Save As",
|
|
57
|
+
"watermark_text": "Watermark Text",
|
|
58
|
+
"key_prompt": "Secret Key (Default: 1)",
|
|
59
|
+
"processing": "Processing",
|
|
60
|
+
"scanning": "Scanning",
|
|
61
|
+
"success_embed": "Success!",
|
|
62
|
+
"fail_embed": "Failed.",
|
|
63
|
+
"report_title": "Report",
|
|
64
|
+
"col_item": "Item",
|
|
65
|
+
"col_info": "Detail",
|
|
66
|
+
"target_file": "Target",
|
|
67
|
+
"key_fingerprint": "Key Fingerprint",
|
|
68
|
+
"status": "Status",
|
|
69
|
+
"evidence": "Evidence",
|
|
70
|
+
"note_title": "NOTICE",
|
|
71
|
+
"note_body": "Verification passed if text outlines are visible.",
|
|
72
|
+
"fail_extract": "Failed: No watermark detected.",
|
|
73
|
+
"error_file": "Invalid path."
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
CURRENT_LANG = "zh"
|
|
78
|
+
|
|
79
|
+
def print_banner():
|
|
80
|
+
"""极致简约 Banner"""
|
|
81
|
+
ascii_art = pyfiglet.figlet_format("Aegis", font="slant")
|
|
82
|
+
console.print(Rule(style="blue"))
|
|
83
|
+
console.print(Align.center(Text(ascii_art, style="bold cyan")))
|
|
84
|
+
console.print(Rule(style="blue"))
|
|
85
|
+
console.print()
|
|
86
|
+
|
|
87
|
+
def interactive_menu():
|
|
88
|
+
"""交互式主菜单"""
|
|
89
|
+
global CURRENT_LANG
|
|
90
|
+
|
|
91
|
+
# 语言选择
|
|
92
|
+
lang_choice = questionary.select(
|
|
93
|
+
"Language / 语言",
|
|
94
|
+
choices=["简体中文", "English"],
|
|
95
|
+
qmark=">"
|
|
96
|
+
).ask()
|
|
97
|
+
|
|
98
|
+
if lang_choice is None: return
|
|
99
|
+
CURRENT_LANG = "zh" if lang_choice == "简体中文" else "en"
|
|
100
|
+
msg = MESSAGES[CURRENT_LANG]
|
|
101
|
+
|
|
102
|
+
print_banner()
|
|
103
|
+
|
|
104
|
+
while True:
|
|
105
|
+
action = questionary.select(
|
|
106
|
+
msg["menu_prompt"],
|
|
107
|
+
choices=[
|
|
108
|
+
msg["menu_embed"],
|
|
109
|
+
msg["menu_extract"],
|
|
110
|
+
msg["menu_exit"]
|
|
111
|
+
],
|
|
112
|
+
qmark=">",
|
|
113
|
+
style=questionary.Style([
|
|
114
|
+
('pointer', 'fg:cyan bold'),
|
|
115
|
+
('highlighted', 'fg:cyan bold'),
|
|
116
|
+
('selected', 'fg:green'),
|
|
117
|
+
])
|
|
118
|
+
).ask()
|
|
119
|
+
|
|
120
|
+
if action is None or "Exit" in action or "退出" in action:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
if "Embed" in action or "嵌入" in action:
|
|
124
|
+
input_file = questionary.path(msg["path_input"] + ":", qmark=">").ask()
|
|
125
|
+
if not input_file or not os.path.exists(input_file):
|
|
126
|
+
console.print(f"[red]{msg['error_file']}[/red]")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
output_file = questionary.text(msg["path_output"] + ":", default=input_file + "_protected.png", qmark=">").ask()
|
|
130
|
+
text = questionary.text(msg["watermark_text"] + ":", qmark=">").ask()
|
|
131
|
+
# 密钥改为普通文本,且无默认值显示
|
|
132
|
+
key = questionary.text(msg["key_prompt"] + ":", qmark=">").ask()
|
|
133
|
+
if not key: key = "1"
|
|
134
|
+
|
|
135
|
+
run_embed(input_file, output_file, text, key)
|
|
136
|
+
|
|
137
|
+
elif "Extract" in action or "提取" in action:
|
|
138
|
+
input_file = questionary.path(msg["path_input"] + ":", qmark=">").ask()
|
|
139
|
+
if not input_file or not os.path.exists(input_file):
|
|
140
|
+
console.print(f"[red]{msg['error_file']}[/red]")
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
key = questionary.text(msg["key_prompt"] + ":", qmark=">").ask()
|
|
144
|
+
if not key: key = "1"
|
|
145
|
+
output = input_file + "_wm.png"
|
|
146
|
+
|
|
147
|
+
run_extract(input_file, output, key)
|
|
148
|
+
|
|
149
|
+
def run_embed(input, output, text, key):
|
|
150
|
+
"""执行嵌入核心逻辑"""
|
|
151
|
+
msg = MESSAGES[CURRENT_LANG]
|
|
152
|
+
ext = input.lower().split('.')[-1]
|
|
153
|
+
|
|
154
|
+
with console.status(f"[bold green]{msg['processing']}[/bold green]...", spinner="dots"):
|
|
155
|
+
try:
|
|
156
|
+
if ext == 'pptx':
|
|
157
|
+
handler = PPTHandler()
|
|
158
|
+
success = handler.process(input, output, text, key=key)
|
|
159
|
+
elif ext in ['png', 'jpg', 'jpeg']:
|
|
160
|
+
handler = ImageHandler()
|
|
161
|
+
success = handler.process(input, output, text, key=key)
|
|
162
|
+
else:
|
|
163
|
+
success = False
|
|
164
|
+
except Exception as e:
|
|
165
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
166
|
+
success = False
|
|
167
|
+
|
|
168
|
+
if success:
|
|
169
|
+
# 使用 Text 对象防止路径字符触发 MarkupError
|
|
170
|
+
res_text = Text(f"{msg['success_embed']} ", style="bold green")
|
|
171
|
+
res_text.append(output, style="underline cyan")
|
|
172
|
+
console.print(res_text)
|
|
173
|
+
else:
|
|
174
|
+
console.print(f"[bold red]{msg['fail_embed']}[/bold red]")
|
|
175
|
+
|
|
176
|
+
def run_extract(input, output, key):
|
|
177
|
+
"""执行提取核心逻辑"""
|
|
178
|
+
msg = MESSAGES[CURRENT_LANG]
|
|
179
|
+
ext = input.lower().split('.')[-1]
|
|
180
|
+
result = None
|
|
181
|
+
|
|
182
|
+
with console.status(f"[bold blue]{msg['scanning']}[/bold blue]...", spinner="earth"):
|
|
183
|
+
try:
|
|
184
|
+
if ext == 'pptx':
|
|
185
|
+
handler = PPTHandler()
|
|
186
|
+
result = handler.extract(input, key=key)
|
|
187
|
+
elif ext in ['png', 'jpg', 'jpeg', 'bmp']:
|
|
188
|
+
handler = ImageHandler()
|
|
189
|
+
result = handler.extract(input, output_wm_path=output, key=key)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
192
|
+
|
|
193
|
+
console.print()
|
|
194
|
+
|
|
195
|
+
if result and os.path.exists(result):
|
|
196
|
+
table = Table(title=msg["report_title"], show_header=True, header_style="bold magenta")
|
|
197
|
+
table.add_column(msg["col_item"], style="cyan")
|
|
198
|
+
table.add_column(msg["col_info"], style="green")
|
|
199
|
+
|
|
200
|
+
table.add_row(msg["target_file"], os.path.basename(input))
|
|
201
|
+
table.add_row(msg["key_fingerprint"], f"SHA256(***{key[-3:] if len(key) >= 3 else key})")
|
|
202
|
+
table.add_row(msg["status"], "SUCCESS")
|
|
203
|
+
table.add_row(msg["evidence"], result)
|
|
204
|
+
|
|
205
|
+
console.print(table)
|
|
206
|
+
console.print(Panel(f"[bold yellow]{msg['note_title']}:[/bold yellow] {msg['note_body']}", border_style="yellow"))
|
|
207
|
+
else:
|
|
208
|
+
console.print(Panel(msg["fail_extract"], border_style="red"))
|
|
209
|
+
|
|
210
|
+
@click.group(invoke_without_command=True)
|
|
211
|
+
@click.pass_context
|
|
212
|
+
def main(ctx):
|
|
213
|
+
"""Aegis: 隐形水印保护工具"""
|
|
214
|
+
if ctx.invoked_subcommand is None:
|
|
215
|
+
interactive_menu()
|
|
216
|
+
else:
|
|
217
|
+
# 命令行模式默认不打印 Banner 以保持静默
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
@main.command()
|
|
221
|
+
@click.option('--input', '-i', required=True)
|
|
222
|
+
@click.option('--output', '-o', required=True)
|
|
223
|
+
@click.option('--text', '-t', required=True)
|
|
224
|
+
@click.option('--key', '-k', default="1", type=str)
|
|
225
|
+
def embed(input, output, text, key):
|
|
226
|
+
"""Embed mode"""
|
|
227
|
+
# 命令行模式也需要确保 Banner 已打印(如果没选交互模式的话)
|
|
228
|
+
print_banner()
|
|
229
|
+
run_embed(input, output, text, key)
|
|
230
|
+
|
|
231
|
+
@main.command()
|
|
232
|
+
@click.option('--input', '-i', required=True)
|
|
233
|
+
@click.option('--output', '-o')
|
|
234
|
+
@click.option('--key', '-k', default="1", type=str)
|
|
235
|
+
def extract(input, output, key):
|
|
236
|
+
"""Extract mode"""
|
|
237
|
+
print_banner()
|
|
238
|
+
run_extract(input, output, key)
|
|
239
|
+
|
|
240
|
+
if __name__ == '__main__':
|
|
241
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class DeepWatermarker:
|
|
2
|
+
"""
|
|
3
|
+
基于深度学习 (Deep Learning) 的水印实现 (高级扩展)。
|
|
4
|
+
未来可集成 ResNet/UNet 模型以抵抗强几何攻击。
|
|
5
|
+
"""
|
|
6
|
+
def __init__(self, model_path=None):
|
|
7
|
+
self.model_path = model_path
|
|
8
|
+
|
|
9
|
+
def embed(self, input_path, output_path, text):
|
|
10
|
+
print("[Info] Deep Learning embedding not implemented yet.")
|
|
11
|
+
# TODO: Load PyTorch model and inference
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def extract(self, input_path):
|
|
15
|
+
print("[Info] Deep Learning extraction not implemented yet.")
|
|
16
|
+
pass
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import cv2
|
|
3
|
+
import hashlib
|
|
4
|
+
import numpy as np
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
import blind_watermark
|
|
7
|
+
from blind_watermark import WaterMark
|
|
8
|
+
|
|
9
|
+
# 关闭 blind-watermark 的欢迎提示信息
|
|
10
|
+
blind_watermark.bw_notes.close()
|
|
11
|
+
|
|
12
|
+
class FrequencyWatermarker:
|
|
13
|
+
"""
|
|
14
|
+
基于频域盲水印 (DCT/DWT) 的核心实现。
|
|
15
|
+
安全增强版:支持字符串密钥 + SHA-256 哈希映射,彻底杜绝撞库。
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, key: str = "1"):
|
|
18
|
+
# 将字符串密钥转为哈希,提取前 8 字节转为大整数作为种子
|
|
19
|
+
# 即使只取一部分,其空间也达到了 2^64,远超 2^32
|
|
20
|
+
hash_digest = hashlib.sha256(str(key).encode()).digest()
|
|
21
|
+
seed = int.from_bytes(hash_digest[:8], byteorder='big') % (2**32)
|
|
22
|
+
|
|
23
|
+
self.pwd_wm = seed
|
|
24
|
+
self.pwd_img = seed
|
|
25
|
+
self.wm_size = (64, 64) # 固定水印图尺寸
|
|
26
|
+
|
|
27
|
+
def _text_to_wm_image(self, text):
|
|
28
|
+
"""将文字转为 64x64 的黑白位图"""
|
|
29
|
+
img = Image.new('1', self.wm_size, 0)
|
|
30
|
+
draw = ImageDraw.Draw(img)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
font = ImageFont.load_default()
|
|
34
|
+
except:
|
|
35
|
+
font = None
|
|
36
|
+
|
|
37
|
+
# 简单的自动换行逻辑:如果长度超过 7 个字符且包含下划线或较长,尝试分两行
|
|
38
|
+
if len(text) > 7:
|
|
39
|
+
mid = len(text) // 2
|
|
40
|
+
# 尝试在中间寻找分割点
|
|
41
|
+
text_lines = [text[:mid], text[mid:]]
|
|
42
|
+
y_offset = 15
|
|
43
|
+
for line in text_lines:
|
|
44
|
+
# 获取行尺寸以居中
|
|
45
|
+
bbox = draw.textbbox((0, 0), line, font=font)
|
|
46
|
+
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
47
|
+
draw.text(((self.wm_size[0] - w) // 2, y_offset), line, font=font, fill=1)
|
|
48
|
+
y_offset += 20
|
|
49
|
+
else:
|
|
50
|
+
# 短文字直接居中
|
|
51
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
52
|
+
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
53
|
+
draw.text(((self.wm_size[0] - w) // 2, (self.wm_size[1] - h) // 2), text, font=font, fill=1)
|
|
54
|
+
|
|
55
|
+
temp_wm_path = "temp_wm.png"
|
|
56
|
+
img.save(temp_wm_path)
|
|
57
|
+
return temp_wm_path
|
|
58
|
+
|
|
59
|
+
def embed(self, input_path: str, output_path: str, text: str):
|
|
60
|
+
"""嵌入水印"""
|
|
61
|
+
temp_wm = self._text_to_wm_image(text)
|
|
62
|
+
try:
|
|
63
|
+
bwm = WaterMark(password_wm=self.pwd_wm, password_img=self.pwd_img)
|
|
64
|
+
bwm.read_img(input_path)
|
|
65
|
+
bwm.read_wm(temp_wm) # 以图片模式读取
|
|
66
|
+
bwm.embed(output_path)
|
|
67
|
+
return True
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"[Error] Failed to embed image watermark: {e}")
|
|
70
|
+
return False
|
|
71
|
+
finally:
|
|
72
|
+
if os.path.exists(temp_wm):
|
|
73
|
+
os.remove(temp_wm)
|
|
74
|
+
|
|
75
|
+
def extract(self, input_path: str, output_wm_path: str = None) -> str:
|
|
76
|
+
"""提取水印"""
|
|
77
|
+
if output_wm_path is None:
|
|
78
|
+
# 在同一目录下生成提取结果
|
|
79
|
+
output_wm_path = input_path + "_wm.png"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
bwm = WaterMark(password_wm=self.pwd_wm, password_img=self.pwd_img)
|
|
83
|
+
# 图片模式提取,固定 shape
|
|
84
|
+
bwm.extract(input_path, wm_shape=self.wm_size, out_wm_name=output_wm_path, mode='img')
|
|
85
|
+
return output_wm_path
|
|
86
|
+
except Exception as e:
|
|
87
|
+
# print(f"[Error] Failed to extract image watermark: {e}")
|
|
88
|
+
return ""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
|
|
3
|
+
class SpatialWatermarker:
|
|
4
|
+
"""
|
|
5
|
+
空域 LSB (Least Significant Bit) 水印实现。
|
|
6
|
+
仅用于教学演示,抗干扰能力弱(转码即丢失),但完全隐形。
|
|
7
|
+
"""
|
|
8
|
+
def embed(self, input_path: str, output_path: str, text: str):
|
|
9
|
+
# 这是一个极简实现的占位符,实际工程中通常不推荐用LSB保护重要版权
|
|
10
|
+
# 这里仅作结构展示
|
|
11
|
+
img = Image.open(input_path)
|
|
12
|
+
# TODO: LSB encoding logic here
|
|
13
|
+
img.save(output_path)
|
|
14
|
+
print(f"[Info] Spatial LSB watermark embedded (Simulation) -> {output_path}")
|
|
15
|
+
|
|
16
|
+
def extract(self, input_path: str):
|
|
17
|
+
print(f"[Info] Spatial LSB watermark extracted (Simulation)")
|
|
18
|
+
return "DEMO_TEXT"
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from aegis.core.frequency import FrequencyWatermarker
|
|
3
|
+
|
|
4
|
+
class ImageHandler:
|
|
5
|
+
def process(self, input_path, output_path, watermark_text, key="1"):
|
|
6
|
+
"""处理单张图片"""
|
|
7
|
+
print(f"[*] Processing Image: {input_path}")
|
|
8
|
+
engine = FrequencyWatermarker(key=key)
|
|
9
|
+
return engine.embed(input_path, output_path, watermark_text)
|
|
10
|
+
|
|
11
|
+
def extract(self, input_path, output_wm_path=None, key="1"):
|
|
12
|
+
"""从单张图片提取"""
|
|
13
|
+
print(f"[*] Extracting from Image: {input_path}")
|
|
14
|
+
engine = FrequencyWatermarker(key=key)
|
|
15
|
+
return engine.extract(input_path, output_wm_path=output_wm_path)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import zipfile
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from aegis.core.frequency import FrequencyWatermarker
|
|
7
|
+
|
|
8
|
+
class PPTHandler:
|
|
9
|
+
"""
|
|
10
|
+
PPTX 处理器:将 PPTX 视为 ZIP 解压,处理内部 media 目录下的图片。
|
|
11
|
+
支持加密(Embed)和解密验证(Extract)。
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self):
|
|
14
|
+
# 忽略小图标、缩略图,避免破坏UI (单位: 字节)
|
|
15
|
+
self.min_file_size = 50 * 1024 # 50KB
|
|
16
|
+
|
|
17
|
+
def process(self, input_path, output_path, watermark_text, key="1"):
|
|
18
|
+
"""给 PPTX 打水印"""
|
|
19
|
+
print(f"[*] Processing PPTX: {input_path}")
|
|
20
|
+
engine = FrequencyWatermarker(key=key)
|
|
21
|
+
|
|
22
|
+
# 创建临时工作目录
|
|
23
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
24
|
+
# 1. 解压 PPTX
|
|
25
|
+
try:
|
|
26
|
+
with zipfile.ZipFile(input_path, 'r') as zip_ref:
|
|
27
|
+
zip_ref.extractall(temp_dir)
|
|
28
|
+
except zipfile.BadZipFile:
|
|
29
|
+
print(f"[Error] Not a valid PPTX/Zip file: {input_path}")
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# 2. 遍历 ppt/media/ 目录
|
|
33
|
+
media_dir = os.path.join(temp_dir, 'ppt', 'media')
|
|
34
|
+
if os.path.exists(media_dir):
|
|
35
|
+
for filename in os.listdir(media_dir):
|
|
36
|
+
file_path = os.path.join(media_dir, filename)
|
|
37
|
+
|
|
38
|
+
# 检查是否为图片且大小足够
|
|
39
|
+
if self._is_target_image(filename) and os.path.getsize(file_path) > self.min_file_size:
|
|
40
|
+
print(f" -> Watermarking inner image: {filename}")
|
|
41
|
+
# 原地替换:读取原图 -> 加水印 -> 覆盖原图
|
|
42
|
+
temp_img = file_path + ".tmp.png"
|
|
43
|
+
success = engine.embed(file_path, temp_img, watermark_text)
|
|
44
|
+
if success:
|
|
45
|
+
os.replace(temp_img, file_path)
|
|
46
|
+
else:
|
|
47
|
+
if os.path.exists(temp_img): os.remove(temp_img)
|
|
48
|
+
|
|
49
|
+
# 3. 重新打包 (Re-zip)
|
|
50
|
+
self._zip_folder(temp_dir, output_path)
|
|
51
|
+
print(f"[+] Success! Protected PPT saved to: {output_path}")
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def extract(self, input_path, key="1"):
|
|
55
|
+
"""
|
|
56
|
+
从 PPTX 中提取水印。
|
|
57
|
+
"""
|
|
58
|
+
print(f"[*] Extracting from PPTX: {input_path}")
|
|
59
|
+
engine = FrequencyWatermarker(key=key)
|
|
60
|
+
|
|
61
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
62
|
+
try:
|
|
63
|
+
with zipfile.ZipFile(input_path, 'r') as zip_ref:
|
|
64
|
+
zip_ref.extractall(temp_dir)
|
|
65
|
+
except zipfile.BadZipFile:
|
|
66
|
+
return "Error: Invalid PPTX file"
|
|
67
|
+
|
|
68
|
+
media_dir = os.path.join(temp_dir, 'ppt', 'media')
|
|
69
|
+
if not os.path.exists(media_dir):
|
|
70
|
+
return "No media found in PPTX"
|
|
71
|
+
|
|
72
|
+
# 遍历图片
|
|
73
|
+
image_files = [f for f in os.listdir(media_dir) if self._is_target_image(f)]
|
|
74
|
+
total_imgs = len(image_files)
|
|
75
|
+
print(f" -> Found {total_imgs} images. Scanning candidates (>50KB)...")
|
|
76
|
+
|
|
77
|
+
for filename in image_files:
|
|
78
|
+
file_path = os.path.join(media_dir, filename)
|
|
79
|
+
# 只检测大图,提高效率且排除干扰
|
|
80
|
+
if os.path.getsize(file_path) > self.min_file_size:
|
|
81
|
+
# 尝试提取
|
|
82
|
+
output_name = os.path.basename(input_path) + "_extracted_wm.png"
|
|
83
|
+
wm_path = engine.extract(file_path, output_wm_path=output_name)
|
|
84
|
+
if wm_path and os.path.exists(wm_path):
|
|
85
|
+
print(f" -> Found trace in {filename}. Saved to: {wm_path}")
|
|
86
|
+
return wm_path
|
|
87
|
+
|
|
88
|
+
return "No watermark detected"
|
|
89
|
+
|
|
90
|
+
def _is_target_image(self, filename):
|
|
91
|
+
ext = filename.lower().split('.')[-1]
|
|
92
|
+
return ext in ['png', 'jpg', 'jpeg', 'bmp', 'tiff']
|
|
93
|
+
|
|
94
|
+
def _zip_folder(self, folder_path, output_path):
|
|
95
|
+
"""将目录压缩回 .pptx 格式"""
|
|
96
|
+
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
97
|
+
for root, dirs, files in os.walk(folder_path):
|
|
98
|
+
for file in files:
|
|
99
|
+
file_path = os.path.join(root, file)
|
|
100
|
+
arcname = os.path.relpath(file_path, folder_path)
|
|
101
|
+
zipf.write(file_path, arcname)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegis-watermark
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A professional hidden watermark tool for images and PPTX documents.
|
|
5
|
+
Home-page: https://github.com/your-repo/Aegis-Watermark
|
|
6
|
+
Author: Aegis Team
|
|
7
|
+
Author-email: admin@aegis-watermark.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: blind-watermark
|
|
14
|
+
Requires-Dist: opencv-python
|
|
15
|
+
Requires-Dist: numpy<2.0.0
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: Pillow
|
|
18
|
+
Requires-Dist: pyfiglet
|
|
19
|
+
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: python-pptx
|
|
21
|
+
Requires-Dist: questionary
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# Aegis (神盾) - 专业级隐形水印保护工具
|
|
33
|
+
|
|
34
|
+
Aegis 是一个基于频域盲水印技术的版权保护工具。它能够为图像及 PPTX 文档嵌入肉眼不可见的“视觉指纹”,即使文件经过截图、压缩或格式转换,依然可以提取出清晰的版权证据。
|
|
35
|
+
|
|
36
|
+
## 🌟 核心亮点
|
|
37
|
+
|
|
38
|
+
- **视觉水印技术**: 不同于脆弱的文本编码,Aegis 嵌入的是视觉轮廓,对图像损伤具有极强的抗性。
|
|
39
|
+
- **SHA-256 安全加固**: 支持任意长度字符串密钥,通过哈希拉伸技术,彻底杜绝暴力破解和撞库。
|
|
40
|
+
- **交互式控制台**: 输入 `aegis` 即可进入专业感十足的中英双语交互式主菜单,无需记忆复杂指令。
|
|
41
|
+
- **全自动文档保护**: 针对 `.pptx` 格式提供深度加固,自动识别并保护文档内部的所有高价值图像素材。
|
|
42
|
+
- **极简专业 UI**: 借鉴现代 CLI 设计,提供 ASCII Banner、动态加载动画及结构化分析报告。
|
|
43
|
+
|
|
44
|
+
## 🛠️ 技术栈
|
|
45
|
+
|
|
46
|
+
- **核心驱动**: `blind-watermark` (频率域 DCT/DWT 变换)
|
|
47
|
+
- **界面引擎**: `Rich` (终端美化) & `questionary` (交互菜单)
|
|
48
|
+
- **图像处理**: `OpenCV`, `Pillow`, `NumPy`
|
|
49
|
+
- **文档处理**: `python-pptx`
|
|
50
|
+
|
|
51
|
+
## 🚀 快速开始
|
|
52
|
+
|
|
53
|
+
### 1. 环境准备
|
|
54
|
+
确保你的系统安装了 Python 3.8+。
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 克隆项目并进入目录
|
|
58
|
+
git clone https://github.com/your-repo/Aegis-Watermark.git
|
|
59
|
+
cd Aegis-Watermark
|
|
60
|
+
|
|
61
|
+
# 安装依赖
|
|
62
|
+
pip install -r requirements.txt
|
|
63
|
+
|
|
64
|
+
# 以开发模式安装
|
|
65
|
+
pip install -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. 运行工具
|
|
69
|
+
在终端输入命令即可启动:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
aegis
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. 使用模式
|
|
76
|
+
|
|
77
|
+
- **交互模式**: 直接输入 `aegis`,跟随菜单引导完成操作。
|
|
78
|
+
- **命令模式**: 支持静默调用,适合脚本集成:
|
|
79
|
+
- 嵌入:`aegis embed -i input.png -o output.png -t "ID:716" -k "MyPassword"`
|
|
80
|
+
- 提取:`aegis extract -i output.png -k "MyPassword"`
|
|
81
|
+
|
|
82
|
+
## 📂 目录结构
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
aegis/
|
|
86
|
+
├── cli.py # 交互式双语 CLI 入口
|
|
87
|
+
├── core/ # 核心算法 (视觉水印 & 哈希加固)
|
|
88
|
+
└── handlers/ # 格式适配 (Image & PPTX)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 🔒 安全建议
|
|
92
|
+
|
|
93
|
+
- **密钥设置**: 建议使用包含字母和数字的长字符串作为密钥。
|
|
94
|
+
- **默认机制**: 若不输入密钥,系统将默认使用密钥 "1"。
|
|
95
|
+
- **版权验证**: 提取出的结果图片若能清晰辨认文字轮廓,即具备版权铁证效力。
|
|
96
|
+
|
|
97
|
+
## 📝 路线图
|
|
98
|
+
|
|
99
|
+
- [ ] 支持 PDF 矢量图层水印
|
|
100
|
+
- [ ] 增加多线程批量处理模式
|
|
101
|
+
- [ ] 集成 AI 深度学习水印算法
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
**Aegis - 为每一份智力成果披上隐形神盾。**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
aegis/__init__.py
|
|
4
|
+
aegis/cli.py
|
|
5
|
+
aegis/core/__init__.py
|
|
6
|
+
aegis/core/deep.py
|
|
7
|
+
aegis/core/frequency.py
|
|
8
|
+
aegis/core/spatial.py
|
|
9
|
+
aegis/handlers/__init__.py
|
|
10
|
+
aegis/handlers/image.py
|
|
11
|
+
aegis/handlers/pdf.py
|
|
12
|
+
aegis/handlers/ppt.py
|
|
13
|
+
aegis_watermark.egg-info/PKG-INFO
|
|
14
|
+
aegis_watermark.egg-info/SOURCES.txt
|
|
15
|
+
aegis_watermark.egg-info/dependency_links.txt
|
|
16
|
+
aegis_watermark.egg-info/entry_points.txt
|
|
17
|
+
aegis_watermark.egg-info/requires.txt
|
|
18
|
+
aegis_watermark.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aegis
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='aegis-watermark',
|
|
8
|
+
version='0.1.0',
|
|
9
|
+
author='Aegis Team',
|
|
10
|
+
author_email='admin@aegis-watermark.com',
|
|
11
|
+
description='A professional hidden watermark tool for images and PPTX documents.',
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url='https://github.com/your-repo/Aegis-Watermark',
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
install_requires=[
|
|
17
|
+
'blind-watermark',
|
|
18
|
+
'opencv-python',
|
|
19
|
+
'numpy<2.0.0',
|
|
20
|
+
'click',
|
|
21
|
+
'Pillow',
|
|
22
|
+
'pyfiglet',
|
|
23
|
+
'rich',
|
|
24
|
+
'python-pptx',
|
|
25
|
+
'questionary'
|
|
26
|
+
],
|
|
27
|
+
classifiers=[
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
],
|
|
32
|
+
python_requires='>=3.8',
|
|
33
|
+
entry_points={
|
|
34
|
+
'console_scripts': [
|
|
35
|
+
'aegis=aegis.cli:main',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
)
|