bitool 0.1.2__py3-none-any.whl
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.
- bitool/__init__.py +27 -0
- bitool/cmd/__init__.py +65 -0
- bitool/cmd/_base.py +105 -0
- bitool/cmd/_condition.py +60 -0
- bitool/cmd/_scheduler.py +548 -0
- bitool/cmd/env.py +454 -0
- bitool/cmd/git.py +123 -0
- bitool/cmd/io.py +248 -0
- bitool/cmd/pdf.py +385 -0
- bitool/cmd/run.py +300 -0
- bitool/cmd/toml.py +237 -0
- bitool/cmd/version.py +630 -0
- bitool/consts.py +14 -0
- bitool/core/__init__.py +7 -0
- bitool/core/app.py +142 -0
- bitool/core/commands.py +194 -0
- bitool/core/config.py +647 -0
- bitool/core/env.py +18 -0
- bitool/core/logger.py +237 -0
- bitool/core/plugin.py +117 -0
- bitool/core/workspace.py +76 -0
- bitool/models/__init__.py +3 -0
- bitool/models/version.py +173 -0
- bitool/scripts/__init__.py +1 -0
- bitool/scripts/bumpversion.py +189 -0
- bitool/scripts/clearscreen.py +37 -0
- bitool/scripts/envpy.py +161 -0
- bitool/scripts/envrs.py +119 -0
- bitool/scripts/filedate.py +246 -0
- bitool/scripts/filelevel.py +191 -0
- bitool/scripts/gittool.py +178 -0
- bitool/scripts/img2pdf.py +151 -0
- bitool/scripts/pdf2img.py +139 -0
- bitool/scripts/piptool.py +130 -0
- bitool/scripts/pymake.py +345 -0
- bitool/scripts/sshcopyid.py +491 -0
- bitool/scripts/taskkill.py +366 -0
- bitool/scripts/which.py +227 -0
- bitool/types.py +7 -0
- bitool/utils/__init__.py +9 -0
- bitool/utils/cli_parser.py +412 -0
- bitool/utils/executor.py +881 -0
- bitool/utils/profiler.py +369 -0
- bitool/utils/task.py +133 -0
- bitool/utils/task_group.py +668 -0
- bitool/utils/tests/__init__.py +0 -0
- bitool/utils/tests/test_profiler.py +487 -0
- bitool-0.1.2.dist-info/METADATA +154 -0
- bitool-0.1.2.dist-info/RECORD +51 -0
- bitool-0.1.2.dist-info/WHEEL +4 -0
- bitool-0.1.2.dist-info/entry_points.txt +15 -0
bitool/cmd/io.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""文件IO命令模块.
|
|
2
|
+
|
|
3
|
+
提供文件读写相关的命令实现.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from bitool.cmd._base import BaseCommand
|
|
13
|
+
from bitool.core import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _backup_file(filepath: Path, backup_path: Path | None = None) -> Path | None:
|
|
17
|
+
"""备份文件辅助函数.
|
|
18
|
+
|
|
19
|
+
参数:
|
|
20
|
+
filepath: 源文件路径
|
|
21
|
+
backup_path: 备份文件路径, 如果为 None 则自动生成
|
|
22
|
+
|
|
23
|
+
返回:
|
|
24
|
+
备份文件路径, 如果备份失败则返回 None
|
|
25
|
+
"""
|
|
26
|
+
# 如果未指定备份路径, 自动生成带 .bak 后缀的路径
|
|
27
|
+
if backup_path is None or str(backup_path) == ".":
|
|
28
|
+
backup_path = filepath.with_suffix(filepath.suffix + ".bak")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# 确保备份文件父目录存在
|
|
32
|
+
if not backup_path.parent.exists():
|
|
33
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
shutil.copy2(filepath, backup_path)
|
|
36
|
+
logger.info(msg=f"已备份文件: `{filepath}` -> `{backup_path}`")
|
|
37
|
+
except OSError as e:
|
|
38
|
+
logger.error(msg=f"备份文件时出错: `{e}`", exc_info=True)
|
|
39
|
+
return None
|
|
40
|
+
else:
|
|
41
|
+
return backup_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class WriteFileCommand(BaseCommand):
|
|
46
|
+
"""写入文件命令."""
|
|
47
|
+
|
|
48
|
+
name = "write_file"
|
|
49
|
+
description = "写入文件命令"
|
|
50
|
+
|
|
51
|
+
filepath: Path = Path()
|
|
52
|
+
content: str = ""
|
|
53
|
+
overwrite: bool = True
|
|
54
|
+
backup: bool = True
|
|
55
|
+
|
|
56
|
+
def run(self) -> bool:
|
|
57
|
+
"""写入文件."""
|
|
58
|
+
# 确保父目录存在
|
|
59
|
+
if not self.filepath.parent.exists():
|
|
60
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
# 处理文件已存在的情况
|
|
63
|
+
if self.filepath.exists():
|
|
64
|
+
if not self.overwrite:
|
|
65
|
+
logger.warning(msg=f"文件已存在且不允许覆盖: `{self.filepath}`")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# 如果需要备份, 使用辅助函数创建备份文件
|
|
69
|
+
if self.backup and _backup_file(self.filepath) is None:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# 写入文件内容
|
|
73
|
+
try:
|
|
74
|
+
self.filepath.write_text(data=self.content, encoding="utf-8")
|
|
75
|
+
except OSError as e:
|
|
76
|
+
logger.error(msg=f"写入文件时出错: `{e}`", exc_info=True)
|
|
77
|
+
return False
|
|
78
|
+
else:
|
|
79
|
+
logger.info(msg=f"写入`{self.filepath}` 成功")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ReadFileCommand(BaseCommand):
|
|
85
|
+
"""读取文件命令."""
|
|
86
|
+
|
|
87
|
+
name = "read_file"
|
|
88
|
+
description = "读取文件命令"
|
|
89
|
+
|
|
90
|
+
filepath: Path = Path()
|
|
91
|
+
content: str = ""
|
|
92
|
+
|
|
93
|
+
def run(self) -> bool:
|
|
94
|
+
"""读取文件内容."""
|
|
95
|
+
if not self.filepath.exists():
|
|
96
|
+
logger.warning(msg=f"文件不存在: `{self.filepath}`")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
if not self.filepath.is_file():
|
|
100
|
+
logger.warning(msg=f"路径不是文件: `{self.filepath}`")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
self.content: str = self.filepath.read_text(encoding="utf-8")
|
|
105
|
+
logger.info(msg=f"读取`{self.filepath}` 成功")
|
|
106
|
+
except OSError as e:
|
|
107
|
+
logger.error(msg=f"读取文件时出错: `{e}`", exc_info=True)
|
|
108
|
+
return False
|
|
109
|
+
else:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class BackupFileCommand(BaseCommand):
|
|
115
|
+
"""备份文件命令."""
|
|
116
|
+
|
|
117
|
+
name = "backup_file"
|
|
118
|
+
description = "备份文件命令"
|
|
119
|
+
|
|
120
|
+
filepath: Path = Path()
|
|
121
|
+
backup_path: Path = Path()
|
|
122
|
+
|
|
123
|
+
def run(self) -> bool:
|
|
124
|
+
"""备份文件."""
|
|
125
|
+
if not self.filepath.exists():
|
|
126
|
+
logger.warning(msg=f"文件不存在: `{self.filepath}`")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
if not self.filepath.is_file():
|
|
130
|
+
logger.warning(msg=f"路径不是文件: `{self.filepath}`")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# 使用辅助函数进行备份
|
|
134
|
+
return _backup_file(self.filepath, self.backup_path) is not None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class CopyFileCommand(BaseCommand):
|
|
139
|
+
"""复制文件命令."""
|
|
140
|
+
|
|
141
|
+
name = "copy_file"
|
|
142
|
+
description = "复制文件命令"
|
|
143
|
+
|
|
144
|
+
source: Path = Path()
|
|
145
|
+
destination: Path = Path()
|
|
146
|
+
overwrite: bool = True
|
|
147
|
+
|
|
148
|
+
def run(self) -> bool:
|
|
149
|
+
"""复制文件."""
|
|
150
|
+
if not self.source.exists():
|
|
151
|
+
logger.warning(msg=f"源文件不存在: `{self.source}`")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
if not self.source.is_file():
|
|
155
|
+
logger.warning(msg=f"源路径不是文件: `{self.source}`")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# 检查目标文件是否已存在
|
|
159
|
+
if self.destination.exists() and not self.overwrite:
|
|
160
|
+
logger.warning(msg=f"目标文件已存在且不允许覆盖: `{self.destination}`")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# 确保目标文件父目录存在
|
|
165
|
+
if not self.destination.parent.exists():
|
|
166
|
+
self.destination.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
shutil.copy2(self.source, self.destination)
|
|
169
|
+
logger.info(msg=f"已复制文件: `{self.source}` -> `{self.destination}`")
|
|
170
|
+
except OSError as e:
|
|
171
|
+
logger.error(msg=f"复制文件时出错: `{e}`", exc_info=True)
|
|
172
|
+
return False
|
|
173
|
+
else:
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class MoveFileCommand(BaseCommand):
|
|
179
|
+
"""移动文件命令."""
|
|
180
|
+
|
|
181
|
+
name = "move_file"
|
|
182
|
+
description = "移动文件命令"
|
|
183
|
+
|
|
184
|
+
source: Path = Path()
|
|
185
|
+
destination: Path = Path()
|
|
186
|
+
overwrite: bool = True
|
|
187
|
+
|
|
188
|
+
def run(self) -> bool:
|
|
189
|
+
"""移动文件."""
|
|
190
|
+
if not self.source.exists():
|
|
191
|
+
logger.warning(msg=f"源文件不存在: `{self.source}`")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
if not self.source.is_file():
|
|
195
|
+
logger.warning(msg=f"源路径不是文件: `{self.source}`")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
# 检查目标文件是否已存在
|
|
199
|
+
if self.destination.exists() and not self.overwrite:
|
|
200
|
+
logger.warning(msg=f"目标文件已存在且不允许覆盖: `{self.destination}`")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# 确保目标文件父目录存在
|
|
205
|
+
if not self.destination.parent.exists():
|
|
206
|
+
self.destination.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
|
|
208
|
+
shutil.move(str(self.source), str(self.destination))
|
|
209
|
+
logger.info(msg=f"已移动文件: `{self.source}` -> `{self.destination}`")
|
|
210
|
+
except OSError as e:
|
|
211
|
+
logger.error(msg=f"移动文件时出错: `{e}`", exc_info=True)
|
|
212
|
+
return False
|
|
213
|
+
else:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class DeleteFileCommand(BaseCommand):
|
|
219
|
+
"""删除文件命令."""
|
|
220
|
+
|
|
221
|
+
name = "delete_file"
|
|
222
|
+
description = "删除文件命令"
|
|
223
|
+
|
|
224
|
+
filepath: Path = Path()
|
|
225
|
+
force: bool = False
|
|
226
|
+
|
|
227
|
+
def run(self) -> bool:
|
|
228
|
+
"""删除文件."""
|
|
229
|
+
if not self.filepath.exists():
|
|
230
|
+
if self.force:
|
|
231
|
+
logger.info(msg=f"文件不存在, 跳过删除: `{self.filepath}`")
|
|
232
|
+
return True
|
|
233
|
+
else:
|
|
234
|
+
logger.warning(msg=f"文件不存在: `{self.filepath}`")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if not self.filepath.is_file():
|
|
238
|
+
logger.warning(msg=f"路径不是文件: `{self.filepath}`")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
self.filepath.unlink()
|
|
243
|
+
logger.info(msg=f"已删除文件: `{self.filepath}`")
|
|
244
|
+
except OSError as e:
|
|
245
|
+
logger.error(msg=f"删除文件时出错: `{e}`", exc_info=True)
|
|
246
|
+
return False
|
|
247
|
+
else:
|
|
248
|
+
return True
|
bitool/cmd/pdf.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""PDF操作模块, 提供用户友好的PDF操作接口."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import fitz # PyMuPDF
|
|
10
|
+
|
|
11
|
+
HAS_PYMUPDF = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
HAS_PYMUPDF = False
|
|
14
|
+
|
|
15
|
+
from bitool.cmd import BaseCommand
|
|
16
|
+
from bitool.core import logger
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CompressPdfCommand",
|
|
20
|
+
"ImagesToPdfCommand",
|
|
21
|
+
"MergePdfsCommand",
|
|
22
|
+
"PdfToImagesCommand",
|
|
23
|
+
"SplitPdfCommand",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class SplitPdfCommand(BaseCommand):
|
|
29
|
+
"""按页数拆分PDF命令
|
|
30
|
+
|
|
31
|
+
将PDF文件按照指定的页数拆分成多个小文件
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name = "split_pdf"
|
|
35
|
+
description = "按页数拆分PDF"
|
|
36
|
+
|
|
37
|
+
input_path: Path = field(default_factory=Path.cwd)
|
|
38
|
+
output_dir: Path = field(default_factory=Path.cwd)
|
|
39
|
+
pages_per_file: int = 1
|
|
40
|
+
output_files: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def run(self) -> bool:
|
|
43
|
+
"""执行PDF拆分操作
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
操作是否成功
|
|
47
|
+
"""
|
|
48
|
+
if not HAS_PYMUPDF:
|
|
49
|
+
logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if self.pages_per_file <= 0:
|
|
53
|
+
logger.error("每文件页数必须大于0")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
if not self.input_path.exists():
|
|
57
|
+
logger.error(f"输入文件不存在: {self.input_path}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# 确保输出目录存在
|
|
62
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
logger.info(
|
|
65
|
+
f"开始拆分PDF: {self.input_path}, 每文件 {self.pages_per_file} 页"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 打开PDF文档
|
|
69
|
+
doc = fitz.open(self.input_path)
|
|
70
|
+
total_pages = len(doc)
|
|
71
|
+
self.output_files = []
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# 计算需要拆分成多少个文件
|
|
75
|
+
num_files = (
|
|
76
|
+
total_pages + self.pages_per_file - 1
|
|
77
|
+
) // self.pages_per_file
|
|
78
|
+
|
|
79
|
+
for i in range(num_files):
|
|
80
|
+
# 创建新文档
|
|
81
|
+
new_doc = fitz.open()
|
|
82
|
+
start_page = i * self.pages_per_file
|
|
83
|
+
end_page = min(start_page + self.pages_per_file, total_pages)
|
|
84
|
+
|
|
85
|
+
# 添加页面到新文档
|
|
86
|
+
for page_num in range(start_page, end_page):
|
|
87
|
+
new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
|
|
88
|
+
|
|
89
|
+
# 保存新文档
|
|
90
|
+
output_path = self.output_dir / f"part_{i + 1}.pdf"
|
|
91
|
+
new_doc.save(str(output_path))
|
|
92
|
+
new_doc.close()
|
|
93
|
+
self.output_files.append(str(output_path))
|
|
94
|
+
|
|
95
|
+
logger.info(f"PDF拆分完成, 生成 {len(self.output_files)} 个文件")
|
|
96
|
+
finally:
|
|
97
|
+
doc.close()
|
|
98
|
+
except FileNotFoundError as e:
|
|
99
|
+
logger.error(f"输入文件不存在: {e}")
|
|
100
|
+
return False
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
logger.error(f"参数错误: {e}")
|
|
103
|
+
return False
|
|
104
|
+
except Exception as e: # noqa: BLE001
|
|
105
|
+
logger.error(f"PDF拆分失败: {e}")
|
|
106
|
+
return False
|
|
107
|
+
else:
|
|
108
|
+
return len(self.output_files) > 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class MergePdfsCommand(BaseCommand):
|
|
113
|
+
"""合并多个PDF文件命令
|
|
114
|
+
|
|
115
|
+
将多个PDF文件合并成一个完整的PDF文件
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
name = "merge_pdfs"
|
|
119
|
+
description = "合并多个PDF文件"
|
|
120
|
+
|
|
121
|
+
input_paths: list[Path] = field(default_factory=list)
|
|
122
|
+
output_path: Path = field(default_factory=lambda: Path.cwd())
|
|
123
|
+
|
|
124
|
+
def run(self) -> bool:
|
|
125
|
+
"""执行PDF合并操作
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
操作是否成功
|
|
129
|
+
"""
|
|
130
|
+
if not HAS_PYMUPDF:
|
|
131
|
+
logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
if not self.input_paths:
|
|
135
|
+
logger.error("输入文件列表为空")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"开始合并 {len(self.input_paths)} 个PDF文件 -> {self.output_path}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 创建新文档
|
|
144
|
+
merged_doc = fitz.open()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# 遍历所有输入文件
|
|
148
|
+
for input_path in self.input_paths:
|
|
149
|
+
if not input_path.exists():
|
|
150
|
+
logger.error(f"输入文件不存在: {input_path}")
|
|
151
|
+
return False
|
|
152
|
+
doc = fitz.open(str(input_path))
|
|
153
|
+
merged_doc.insert_pdf(doc)
|
|
154
|
+
doc.close()
|
|
155
|
+
|
|
156
|
+
# 确保输出目录存在
|
|
157
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
# 保存合并后的文档
|
|
160
|
+
merged_doc.save(str(self.output_path))
|
|
161
|
+
logger.info(f"PDF合并完成: {self.output_path}")
|
|
162
|
+
finally:
|
|
163
|
+
merged_doc.close()
|
|
164
|
+
except FileNotFoundError as e:
|
|
165
|
+
logger.error(f"输入文件不存在: {e}")
|
|
166
|
+
return False
|
|
167
|
+
except Exception as e: # noqa: BLE001
|
|
168
|
+
logger.error(f"PDF合并失败: {e}")
|
|
169
|
+
return False
|
|
170
|
+
else:
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class CompressPdfCommand(BaseCommand):
|
|
176
|
+
"""压缩PDF文件命令
|
|
177
|
+
|
|
178
|
+
对PDF文件进行压缩以减小文件大小
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
name = "compress_pdf"
|
|
182
|
+
description = "压缩PDF文件"
|
|
183
|
+
|
|
184
|
+
input_path: Path = field(default_factory=Path.cwd)
|
|
185
|
+
output_path: Path = field(default_factory=Path.cwd)
|
|
186
|
+
quality: int = 75
|
|
187
|
+
|
|
188
|
+
def run(self) -> bool:
|
|
189
|
+
"""执行PDF压缩操作
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
操作是否成功
|
|
193
|
+
"""
|
|
194
|
+
if not HAS_PYMUPDF:
|
|
195
|
+
logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
if not (1 <= self.quality <= 100):
|
|
199
|
+
logger.error("压缩质量必须在1-100之间")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
if not self.input_path.exists():
|
|
203
|
+
logger.error(f"输入文件不存在: {self.input_path}")
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
logger.info(f"开始压缩PDF: {self.input_path} -> {self.output_path}")
|
|
208
|
+
|
|
209
|
+
# 打开PDF文档
|
|
210
|
+
doc = fitz.open(self.input_path)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# 确保输出目录存在
|
|
214
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
|
|
216
|
+
# 使用PyMuPDF的压缩功能
|
|
217
|
+
doc.save(
|
|
218
|
+
str(self.output_path),
|
|
219
|
+
garbage=4, # 删除未使用的对象
|
|
220
|
+
deflate=True, # 压缩流
|
|
221
|
+
clean=True, # 清理内容
|
|
222
|
+
)
|
|
223
|
+
logger.info(f"PDF压缩完成: {self.output_path}")
|
|
224
|
+
finally:
|
|
225
|
+
doc.close()
|
|
226
|
+
except FileNotFoundError as e:
|
|
227
|
+
logger.error(f"输入文件不存在: {e}")
|
|
228
|
+
return False
|
|
229
|
+
except ValueError as e:
|
|
230
|
+
logger.error(f"参数错误: {e}")
|
|
231
|
+
return False
|
|
232
|
+
except Exception as e: # noqa: BLE001
|
|
233
|
+
logger.error(f"PDF压缩失败: {e}")
|
|
234
|
+
return False
|
|
235
|
+
else:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class PdfToImagesCommand(BaseCommand):
|
|
241
|
+
"""将PDF转换为图片命令
|
|
242
|
+
|
|
243
|
+
将PDF文件的每一页转换为图片格式
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
name = "pdf_to_images"
|
|
247
|
+
description = "将PDF转换为图片"
|
|
248
|
+
|
|
249
|
+
input_path: Path = field(default_factory=lambda: Path.cwd())
|
|
250
|
+
output_dir: Path = field(default_factory=lambda: Path.cwd())
|
|
251
|
+
fmt: str = "png"
|
|
252
|
+
dpi: int = 150
|
|
253
|
+
output_files: list[str] = field(default_factory=list)
|
|
254
|
+
|
|
255
|
+
def run(self) -> bool:
|
|
256
|
+
"""执行PDF转图片操作
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
操作是否成功
|
|
260
|
+
"""
|
|
261
|
+
if not HAS_PYMUPDF:
|
|
262
|
+
logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
if not self.input_path.exists():
|
|
266
|
+
logger.error(f"输入文件不存在: {self.input_path}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
logger.info(f"开始转换PDF为图片: {self.input_path} -> {self.output_dir}")
|
|
271
|
+
|
|
272
|
+
# 确保输出目录存在
|
|
273
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
|
|
275
|
+
# 打开PDF文档
|
|
276
|
+
doc = fitz.open(str(self.input_path))
|
|
277
|
+
self.output_files = []
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# 计算缩放比例 (DPI / 72是PDF的默认分辨率)
|
|
281
|
+
zoom = self.dpi / 72.0
|
|
282
|
+
mat = fitz.Matrix(zoom, zoom)
|
|
283
|
+
|
|
284
|
+
# 遍历每一页
|
|
285
|
+
for page_num in range(len(doc)):
|
|
286
|
+
page = doc[page_num]
|
|
287
|
+
|
|
288
|
+
# 渲染页面为图片
|
|
289
|
+
pix = page.get_pixmap(matrix=mat)
|
|
290
|
+
|
|
291
|
+
# 保存图片
|
|
292
|
+
output_path = self.output_dir / f"page_{page_num + 1}.{self.fmt}"
|
|
293
|
+
pix.save(str(output_path))
|
|
294
|
+
self.output_files.append(str(output_path))
|
|
295
|
+
|
|
296
|
+
logger.info(f"PDF转图片完成, 生成 {len(self.output_files)} 个文件")
|
|
297
|
+
finally:
|
|
298
|
+
doc.close()
|
|
299
|
+
except FileNotFoundError as e:
|
|
300
|
+
logger.error(f"输入文件不存在: {e}")
|
|
301
|
+
return False
|
|
302
|
+
except ValueError as e:
|
|
303
|
+
logger.error(f"参数错误: {e}")
|
|
304
|
+
return False
|
|
305
|
+
except Exception as e: # noqa: BLE001
|
|
306
|
+
logger.error(f"PDF转图片失败: {e}")
|
|
307
|
+
return False
|
|
308
|
+
else:
|
|
309
|
+
return len(self.output_files) > 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class ImagesToPdfCommand(BaseCommand):
|
|
314
|
+
"""将图片列表转换为PDF命令
|
|
315
|
+
|
|
316
|
+
将多张图片合并成一个PDF文件
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
name = "images_to_pdf"
|
|
320
|
+
description = "将图片列表转换为PDF"
|
|
321
|
+
|
|
322
|
+
image_paths: list[Path] = field(default_factory=list)
|
|
323
|
+
output_path: Path = field(default_factory=lambda: Path.cwd())
|
|
324
|
+
page_size: str = "auto"
|
|
325
|
+
|
|
326
|
+
def run(self) -> bool:
|
|
327
|
+
"""执行图片转PDF操作
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
操作是否成功
|
|
331
|
+
"""
|
|
332
|
+
if not HAS_PYMUPDF:
|
|
333
|
+
logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
if not self.image_paths:
|
|
337
|
+
logger.error("图片列表为空")
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
logger.info(
|
|
342
|
+
f"开始将 {len(self.image_paths)} 张图片转换为PDF: {self.output_path}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# 创建新PDF文档
|
|
346
|
+
pdf_doc = fitz.open()
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
# 遍历所有图片
|
|
350
|
+
for image_path in self.image_paths:
|
|
351
|
+
if not image_path.exists():
|
|
352
|
+
logger.error(f"图片文件不存在: {image_path}")
|
|
353
|
+
return False
|
|
354
|
+
# 打开图片
|
|
355
|
+
img_doc = fitz.open(str(image_path))
|
|
356
|
+
|
|
357
|
+
# 将图片转换为PDF页面
|
|
358
|
+
pdf_bytes = img_doc.convert_to_pdf()
|
|
359
|
+
img_pdf = fitz.open("pdf", pdf_bytes)
|
|
360
|
+
|
|
361
|
+
# 插入到主文档
|
|
362
|
+
pdf_doc.insert_pdf(img_pdf)
|
|
363
|
+
|
|
364
|
+
img_doc.close()
|
|
365
|
+
img_pdf.close()
|
|
366
|
+
|
|
367
|
+
# 确保输出目录存在
|
|
368
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
369
|
+
|
|
370
|
+
# 保存PDF
|
|
371
|
+
pdf_doc.save(str(self.output_path))
|
|
372
|
+
logger.info(f"图片转PDF完成: {self.output_path}")
|
|
373
|
+
finally:
|
|
374
|
+
pdf_doc.close()
|
|
375
|
+
except FileNotFoundError as e:
|
|
376
|
+
logger.error(f"输入文件不存在: {e}")
|
|
377
|
+
return False
|
|
378
|
+
except ValueError as e:
|
|
379
|
+
logger.error(f"参数错误: {e}")
|
|
380
|
+
return False
|
|
381
|
+
except Exception as e: # noqa: BLE001
|
|
382
|
+
logger.error(f"图片转PDF失败: {e}")
|
|
383
|
+
return False
|
|
384
|
+
else:
|
|
385
|
+
return True
|