bcmd 0.6.6__py3-none-any.whl → 0.6.8__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.

Potentially problematic release.


This version of bcmd might be problematic. Click here for more details.

bcmd/tasks/image.py ADDED
@@ -0,0 +1,376 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import random
6
+ import tkinter as tk
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Final, List, Tuple
10
+
11
+ import httpx
12
+ import typer
13
+ from beni import bcolor, bfile, bhttp, binput, block, bpath, btask
14
+ from beni.bbyte import BytesReader, BytesWriter
15
+ from beni.bfunc import syncCall
16
+ from beni.btype import Null, XPath
17
+ from PIL import Image, ImageDraw, ImageFont
18
+
19
+ from bcmd.utils.tkUtil import TkForm, setWidgetEnabled
20
+ from tkinter import messagebox
21
+
22
+ app: Final = btask.newSubApp('图片工具集')
23
+
24
+
25
+ class _OutputType(StrEnum):
26
+ '输出类型'
27
+ normal = '0'
28
+ replace_ = '1'
29
+ crc_replace = '2'
30
+
31
+
32
+ @app.command()
33
+ @syncCall
34
+ async def convert(
35
+ path: Path = typer.Option(None, '--path', '-p', help='指定目录或具体图片文件,默认当前目录'),
36
+ src_format: str = typer.Option('jpg|jpeg|png', '--src-format', '-s', help='如果path是目录,指定源格式,可以指定多个,默认值:jpg|jpeg|png'),
37
+ dst_format: str = typer.Option('webp', '--dst-format', '-d', help='目标格式,只能是单个'),
38
+ rgb: bool = typer.Option(False, '--rgb', help='转换为RGB格式'),
39
+ quality: int = typer.Option(85, '--quality', '-q', help='图片质量,0-100,默认85'),
40
+ output_type: _OutputType = typer.Option(_OutputType.normal, '--output-type', help='输出类型,0:普通输出,1:删除源文件,2:输出文件使用CRC32命名并删除源文件'),
41
+ ):
42
+ '图片格式转换'
43
+ path = path or Path(os.getcwd())
44
+ fileList: list[Path] = []
45
+ if path.is_file():
46
+ fileList.append(path)
47
+ elif path.is_dir():
48
+ extNameList = [x for x in src_format.strip().split('|')]
49
+ fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in extNameList]
50
+ if not fileList:
51
+ return bcolor.printRed(f'未找到图片文件({path})')
52
+ for file in fileList:
53
+ with Image.open(file) as img:
54
+ if rgb:
55
+ img = img.convert('RGB')
56
+ with bpath.useTempFile() as tempFile:
57
+ img.save(tempFile, format=dst_format, quality=quality)
58
+ outputFile = file.with_suffix(f'.{dst_format}')
59
+ if output_type == _OutputType.crc_replace:
60
+ outputFile = outputFile.with_stem(await bfile.crc(tempFile))
61
+ bpath.copy(tempFile, outputFile)
62
+ if output_type in [_OutputType.replace_, _OutputType.crc_replace]:
63
+ if outputFile != file:
64
+ bpath.remove(file)
65
+ bcolor.printGreen(f'{file} -> {outputFile}')
66
+
67
+
68
+ # ------------------------------------------------------------------------------------
69
+
70
+ @app.command()
71
+ @syncCall
72
+ async def tiny(
73
+ path: Path = typer.Option(None, '--path', help='指定目录或具体图片文件,默认当前目录'),
74
+ optimization: int = typer.Option(25, '--optimization', help='指定优化大小,如果没有达到优化效果就不处理,单位:%,默认25'),
75
+ isKeepOriginal: bool = typer.Option(False, '--keep-original', help='保留原始图片'),
76
+ ):
77
+
78
+ keyList = [
79
+ 'MB3QmtvZ8HKRkXcDnxhWCNTXzvx6cNF3',
80
+ '7L7X2CJ35GM1bChSHdT14yZPLx7FlpNk',
81
+ 'q8YLcvrXVW2NYcr5mMyzQhsSHF4j7gny',
82
+ ]
83
+ random.shuffle(keyList)
84
+
85
+ class _TinyFile:
86
+
87
+ _endian: Final = '>'
88
+ _sep = BytesWriter(_endian).writeStr('tiny').writeUint(9527).writeUint(709394).toBytes()
89
+
90
+ @property
91
+ def compression(self):
92
+ return self._compression
93
+
94
+ @property
95
+ def isTiny(self):
96
+ return self._isTiny
97
+
98
+ @property
99
+ def file(self):
100
+ return self._file
101
+
102
+ def __init__(self, file: XPath):
103
+ self._file = file
104
+ self._compression: float = 0.0
105
+ self._isTiny: bool = False
106
+
107
+ def getSizeDisplay(self):
108
+ size = bpath.get(self._file).stat().st_size / 1024
109
+ return f'{size:,.2f}KB'
110
+
111
+ async def updateInfo(self):
112
+ fileBytes = await bfile.readBytes(self._file)
113
+ self._compression = 0.0
114
+ self._isTiny = False
115
+ blockAry = fileBytes.split(self._sep)
116
+ if len(blockAry) > 1:
117
+ info = BytesReader(self._endian, blockAry[1])
118
+ size = info.readUint()
119
+ if size == len(blockAry[0]):
120
+ self._compression = round(info.readFloat(), 2)
121
+ self._isTiny = info.readBool()
122
+
123
+ async def _flushInfo(self, compression: float, isTiny: bool):
124
+ self._compression = compression
125
+ self._isTiny = isTiny
126
+ content = await self._getPureContent()
127
+ info = (
128
+ BytesWriter(self._endian)
129
+ .writeUint(len(content))
130
+ .writeFloat(compression)
131
+ .writeBool(isTiny)
132
+ .toBytes()
133
+ )
134
+ content += self._sep + info
135
+ await bfile.writeBytes(self._file, content)
136
+
137
+ async def _getPureContent(self):
138
+ content = await bfile.readBytes(self._file)
139
+ content = content.split(self._sep)[0]
140
+ return content
141
+
142
+ @block.limit(1)
143
+ async def runTiny(self, key: str, compression: float, isKeepOriginal: bool):
144
+ content = await self._getPureContent()
145
+ async with httpx.AsyncClient() as client:
146
+ response = await client.post(
147
+ 'https://api.tinify.com/shrink',
148
+ auth=('api', key),
149
+ content=content,
150
+ timeout=30,
151
+ )
152
+ response.raise_for_status()
153
+ result = response.json()
154
+ outputCompression = round(result['output']['ratio'] * 100, 2)
155
+ if outputCompression < compression:
156
+ # 下载文件
157
+ url = result['output']['url']
158
+ with bpath.useTempFile() as tempFile:
159
+ await bhttp.download(url, tempFile)
160
+ await _TinyFile(tempFile)._flushInfo(outputCompression, True)
161
+ outputFile = bpath.get(self._file)
162
+ if isKeepOriginal:
163
+ outputFile = outputFile.with_stem(f'{outputFile.stem}_tiny')
164
+ bpath.move(tempFile, outputFile, True)
165
+ bcolor.printGreen(f'{outputFile}({outputCompression - 100:.2f}% / 压缩 / {self.getSizeDisplay()})')
166
+ else:
167
+ # 不进行压缩
168
+ await self._flushInfo(outputCompression, False)
169
+ bcolor.printMagenta(f'{self._file} ({outputCompression - 100:.2f}% / 不处理 / {self.getSizeDisplay()})')
170
+
171
+ btask.assertTrue(0 < optimization < 100, '优化大小必须在0-100之间')
172
+ compression = 100 - optimization
173
+ await block.setLimit(_TinyFile.runTiny, len(keyList))
174
+
175
+ # 整理文件列表
176
+ fileList = []
177
+ path = path or Path(os.getcwd())
178
+ if path.is_file():
179
+ fileList.append(path)
180
+ elif path.is_dir():
181
+ fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in ['jpg', 'jpeg', 'png']]
182
+ else:
183
+ btask.abort('未找到图片文件', path)
184
+ fileList.sort()
185
+
186
+ # 将文件列表整理成 _TinyFile 对象
187
+ fileList = [_TinyFile(x) for x in fileList]
188
+ await asyncio.gather(*[x.updateInfo() for x in fileList])
189
+
190
+ # 过滤掉已经处理过的图片
191
+ for i in range(len(fileList)):
192
+ file = fileList[i]
193
+ if file.compression == 0:
194
+ # 未处理过的图片,进行图片的压缩处理
195
+ pass
196
+ elif not file.isTiny and file.compression < compression:
197
+ # 之前测试的压缩率不符合要求,不过现在符合了,进行图片的压缩处理
198
+ pass
199
+ else:
200
+ # 要忽略掉的文件
201
+ if file.isTiny:
202
+ bcolor.printYellow(f'{file.file}({file.compression - 100:.2f}% / 已压缩 / {file.getSizeDisplay()})')
203
+ else:
204
+ bcolor.printMagenta(f'{file.file}({file.compression - 100:.2f}% / 不处理 / {file.getSizeDisplay()})')
205
+
206
+ fileList[i] = Null
207
+ fileList = [x for x in fileList if x]
208
+
209
+ # 开始压缩
210
+ taskList = [
211
+ x.runTiny(keyList[i % len(keyList)], compression, isKeepOriginal)
212
+ for i, x in enumerate(fileList)
213
+ ]
214
+ await asyncio.gather(*taskList)
215
+
216
+
217
+ @app.command()
218
+ @syncCall
219
+ async def merge(
220
+ path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
221
+ force: bool = typer.Option(False, '--force', '-f', help='强制覆盖'),
222
+ ):
223
+ '合并多张图片'
224
+
225
+ def _get_watermark_font(font_size: int) -> ImageFont.FreeTypeFont:
226
+ font_candidates = [
227
+ 'arial.ttf', # Windows
228
+ 'Arial.ttf', # macOS(部分系统可能区分大小写)
229
+ 'Helvetica.ttc', # macOS
230
+ 'DejaVuSans.ttf', # Linux
231
+ 'FreeSans.ttf', # 某些Linux发行版
232
+ 'LiberationSans-Regular.ttf' # RHEL系发行版
233
+ ]
234
+ for font_name in font_candidates:
235
+ try:
236
+ return ImageFont.truetype(font_name, font_size)
237
+ except (IOError, OSError):
238
+ continue
239
+ raise Exception('No font found!')
240
+
241
+ def _add_watermark(image: Image.Image, text: str, position: Tuple[float, float]) -> Image.Image:
242
+ """添加圆形背景水印"""
243
+ draw = ImageDraw.Draw(image)
244
+ font_size = 100
245
+ font = _get_watermark_font(font_size)
246
+
247
+ # 计算文本尺寸并确定圆形参数
248
+ text_bbox = draw.textbbox(position, text, font=font)
249
+ text_width = text_bbox[2] - text_bbox[0]
250
+ text_height = text_bbox[3] - text_bbox[1]
251
+
252
+ # 计算圆形参数(直径取文本宽高的最大值 + 边距)
253
+ diameter = max(text_width, text_height) + 20 # 10像素边距
254
+ radius = diameter // 2
255
+
256
+ # 计算圆形中心坐标(保持与原矩形左上角一致)
257
+ circle_center = (
258
+ position[0] + text_width // 2 + 5, # 原位置向右偏移边距
259
+ position[1] + text_height // 2 + 5 # 原位置向下偏移边距
260
+ )
261
+
262
+ # 绘制圆形背景
263
+ ellipse_box = (
264
+ circle_center[0] - radius,
265
+ circle_center[1] - radius,
266
+ circle_center[0] + radius,
267
+ circle_center[1] + radius
268
+ )
269
+ draw.ellipse(ellipse_box, fill='#B920D9')
270
+
271
+ # 绘制居中文字
272
+ draw.text(
273
+ circle_center,
274
+ text,
275
+ (255, 255, 255),
276
+ font=font,
277
+ anchor="mm" # 设置锚点为水平垂直居中
278
+ )
279
+ return image
280
+
281
+ def _merge_images(image_paths: List[Path], output_path: Path) -> None:
282
+ images = [Image.open(img_path) for img_path in image_paths]
283
+ max_width = max(img.width for img in images)
284
+ total_height = sum(img.height for img in images)
285
+ merged_image = Image.new('RGB', (max_width, total_height))
286
+ y_offset = 0
287
+ for idx, img in enumerate(images):
288
+ merged_image.paste(img, (0, y_offset))
289
+ _add_watermark(merged_image, f'{idx + 1}', (22, y_offset + 17))
290
+ y_offset += img.height
291
+ # 修改保存参数为 WebP 格式
292
+ merged_image.save(
293
+ output_path,
294
+ format='JPEG',
295
+ quality=80, # 质量参数(0-100),推荐 80-90 之间
296
+ method=6, # 压缩方法(0-6),6 为最佳压缩
297
+ lossless=False, # 不使用无损压缩(更小的文件体积)
298
+ )
299
+
300
+ image_files = [x for x in bpath.listFile(path) if x.suffix in ('.png', '.jpg', '.jpeg', '.webp', '.bmp')]
301
+ output_image = path / f'merge_{path.name}.jpg' # 修改文件扩展名为 webp
302
+ if output_image in image_files:
303
+ if not force:
304
+ print(output_image)
305
+ await binput.confirm(f'生成文件已经存在,是否覆盖?')
306
+ image_files.remove(output_image)
307
+ image_files.sort(key=lambda x: x.as_posix())
308
+ _merge_images(image_files, output_image)
309
+ bcolor.printMagenta(output_image)
310
+ bcolor.printGreen('OK')
311
+
312
+
313
+ @app.command()
314
+ @syncCall
315
+ async def xone():
316
+ def showGui():
317
+ app = TkForm()
318
+ app.title('图片操作')
319
+ entry = app.addEntry('保存文件', tk.StringVar(value=r'C:\project\docs\source\docs\public\icon.webp'), width=60)
320
+ app.addEntry('输入密码', tk.StringVar(), password=True, width=60, command=lambda: messagebox.showerror('错误', '密码错误'))
321
+
322
+ scrolltextVar = tk.StringVar(value='xxiioo')
323
+ app.addScrolledText('测试信箱', scrolltextVar)
324
+
325
+
326
+ radioVarScale = app.addRadioBtnList(
327
+ '缩放操作',
328
+ [
329
+ '保持',
330
+ '缩放比',
331
+ '指定宽度',
332
+ '指定高度',
333
+ ],
334
+ selectedIndex=0,
335
+ onChanged=lambda x: onScaleValueChanged(x),
336
+ )
337
+
338
+ scaleValueEntry = app.addEntry('缩放参数', tk.StringVar(), width=10, justify=tk.CENTER)
339
+
340
+ app.addChoisePath('选择文件', tk.StringVar(), isDir=True, focus=True)
341
+
342
+ radioVarFormat = app.addRadioBtnList(
343
+ '格式转换',
344
+ [
345
+ '保持',
346
+ 'PNG',
347
+ 'JPEG',
348
+ 'WEBP',
349
+ ],
350
+ selectedIndex=0,
351
+ )
352
+ app.addCheckBox('去除透明', '去除透明', False)
353
+ app.addCheckBox('TinyPng', 'TinyPng', True)
354
+ app.addCheckBoxList('其他选项', [
355
+ ('去除透明', False),
356
+ ('TinyPng', True),
357
+ ])
358
+
359
+ def onScaleValueChanged(value: str):
360
+ match value:
361
+ case '保持':
362
+ setWidgetEnabled(scaleValueEntry, False)
363
+ case _:
364
+ setWidgetEnabled(scaleValueEntry, True)
365
+
366
+ app.addBtn('确定', lambda: onBtn())
367
+
368
+ def onBtn():
369
+ nonlocal result
370
+ app.destroy()
371
+
372
+ result: str = ''
373
+ app.run()
374
+ return result
375
+
376
+ showGui()
bcmd/tasks/json.py ADDED
@@ -0,0 +1,25 @@
1
+ import json
2
+ from typing import Final
3
+
4
+ import pyperclip
5
+ from beni import bcolor, btask
6
+ from beni.bfunc import syncCall
7
+ from rich.console import Console
8
+
9
+ app: Final = btask.app
10
+
11
+
12
+ @app.command('json')
13
+ @syncCall
14
+ async def format_json():
15
+ '格式化 JSON (使用复制文本)'
16
+ content = pyperclip.paste()
17
+ try:
18
+ Console().print_json(content, indent=4, ensure_ascii=False, sort_keys=True)
19
+ data = json.loads(content)
20
+ pyperclip.copy(
21
+ json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
22
+ )
23
+ except:
24
+ bcolor.printRed('无效的 JSON')
25
+ bcolor.printRed(content)
bcmd/tasks/lib.py ADDED
@@ -0,0 +1,94 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Final
4
+
5
+ import typer
6
+ from beni import bcolor, bfile, bpath, brun, btask
7
+ from beni.bfunc import syncCall
8
+
9
+ from bcmd.utils.tkUtil import TkForm
10
+
11
+ from ..common import secret
12
+
13
+ app: Final = btask.newSubApp('lib 工具')
14
+
15
+
16
+ @app.command()
17
+ @syncCall
18
+ async def update_version(
19
+ path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
20
+ isNotCommit: bool = typer.Option(False, '--no-commit', '-d', help='是否提交git'),
21
+ ):
22
+ '修改 pyproject.toml 版本号'
23
+ file = path / 'pyproject.toml'
24
+ btask.assertTrue(file.is_file(), '文件不存在', file)
25
+ data = await bfile.readToml(file)
26
+ latestVersion = data['project']['version']
27
+ vAry = [int(x) for x in latestVersion.split('.')]
28
+ versionList = [
29
+ f'{vAry[0] + 1}.0.0',
30
+ f'{vAry[0]}.{vAry[1] + 1}.0',
31
+ f'{vAry[0]}.{vAry[1]}.{vAry[2] + 1}',
32
+ ]
33
+
34
+ def showGui():
35
+ app = TkForm()
36
+ app.title('bcmd 版本更新')
37
+ app.addLabel('当前版本号', latestVersion)
38
+ version_var = app.addRadioBtnList(
39
+ '请选择新版本',
40
+ versionList,
41
+ selectedIndex=-1,
42
+ )
43
+ result: str = ''
44
+
45
+ def onBtn():
46
+ nonlocal result
47
+ result = version_var.get()
48
+ app.destroy()
49
+
50
+ app.addBtn('确定', onBtn, focus=True)
51
+ app.run()
52
+ return result
53
+
54
+ newVersion = showGui()
55
+ if not newVersion:
56
+ btask.abort('用户取消操作')
57
+ content = await bfile.readText(file)
58
+ if f"version = '{latestVersion}'" in content:
59
+ content = content.replace(f"version = '{latestVersion}'", f"version = '{newVersion}'")
60
+ elif f'version = "{latestVersion}"' in content:
61
+ content = content.replace(f'version = "{latestVersion}"', f'version = "{newVersion}"')
62
+ else:
63
+ raise Exception('版本号修改失败,先检查文件中定义的版本号格式是否正常')
64
+ await bfile.writeText(file, content)
65
+
66
+ # 执行一遍 uv.lock
67
+ with bpath.changePath(path):
68
+ await brun.run('uv lock')
69
+
70
+ bcolor.printCyan(newVersion)
71
+ if not isNotCommit:
72
+ msg = f'更新版本号 {newVersion}'
73
+ os.system(
74
+ rf'TortoiseGitProc.exe /command:commit /path:{path} /logmsg:"{msg}"'
75
+ )
76
+ bcolor.printGreen('OK')
77
+
78
+
79
+ @app.command()
80
+ @syncCall
81
+ async def build(
82
+ path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
83
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
84
+ ):
85
+ '发布项目'
86
+ data = await secret.getPypi(secretValue)
87
+ bpath.remove(path / 'dist')
88
+ bpath.remove(
89
+ *list(path.glob('*.egg-info'))
90
+ )
91
+ with bpath.changePath(path):
92
+ await brun.run(f'uv build')
93
+ await brun.run(f'uv publish -u {data['username']} -p {data['password']}')
94
+ bcolor.printGreen('OK')
bcmd/tasks/math.py ADDED
@@ -0,0 +1,97 @@
1
+ from typing import Final
2
+
3
+ import typer
4
+ from beni import bcolor, btask
5
+ from beni.bfunc import Counter, syncCall, toFloat
6
+ from prettytable import PrettyTable
7
+
8
+ app: Final = btask.newSubApp('math 工具集')
9
+
10
+
11
+ @app.command()
12
+ @syncCall
13
+ async def scale(
14
+ a: float = typer.Argument(..., help='原始数值'),
15
+ b: float = typer.Argument(..., help='原始数值'),
16
+ c: str = typer.Argument(..., help='数值 或 ?'),
17
+ d: str = typer.Argument(..., help='数值 或 ?'),
18
+ ):
19
+ '按比例计算数值,例子:beni math scale 1 2 3 ?'
20
+ if not ((c == '?') != (d == '?')):
21
+ return bcolor.printRed('参数C和参数D必须有且仅有一个为?')
22
+ print()
23
+ table = PrettyTable(
24
+ title=bcolor.yellow('按比例计算数值'),
25
+ )
26
+ if c == '?':
27
+ dd = toFloat(d)
28
+ cc = a * dd / b
29
+ table.add_rows([
30
+ ['A', a, bcolor.magenta(str(cc)), bcolor.magenta('C')],
31
+ ['B', b, dd, 'D'],
32
+ ])
33
+ elif d == '?':
34
+ cc = toFloat(c)
35
+ dd = b * cc / a
36
+ table.add_rows([
37
+ ['A', a, cc, 'C'],
38
+ ['B', b, bcolor.magenta(str(dd)), bcolor.magenta('D')],
39
+ ])
40
+ print(table.get_string(header=False))
41
+
42
+
43
+ @app.command()
44
+ @syncCall
45
+ async def discount(
46
+ values: list[str] = typer.Argument(..., help='每组数据使用#作为分隔符,注意后面的数据不能为0,例:123#500'),
47
+ ):
48
+ '计算折扣,例子:beni math discount 123#500 130#550'
49
+ btask.assertTrue(len(values) >= 2, '至少需要提供2组数据用作比较')
50
+
51
+ class Data:
52
+ def __init__(self, value: str):
53
+ try:
54
+ ary = [x.strip() for x in value.strip().split('#')]
55
+ self.a = float(ary[0])
56
+ self.b = float(ary[1])
57
+ self.v = self.a / self.b
58
+ self.discount = 0.0
59
+ except:
60
+ btask.abort(f'数据格式错误', value)
61
+
62
+ datas = [Data(x) for x in values]
63
+ table = PrettyTable(
64
+ title=bcolor.yellow('计算折扣'),
65
+ )
66
+ vAry = [x.v for x in datas]
67
+ minV = min(vAry)
68
+ maxV = max(vAry)
69
+ for data in datas:
70
+ data.discount = -(maxV - data.v) / maxV
71
+ table.add_column(
72
+ '',
73
+ [
74
+ '前数据',
75
+ '后数据',
76
+ '单价',
77
+ '折扣',
78
+ ],
79
+ )
80
+ counter = Counter(-1)
81
+ for data in datas:
82
+ colorFunc = bcolor.white
83
+ if data.v == minV:
84
+ colorFunc = bcolor.green
85
+ elif data.v == maxV:
86
+ colorFunc = bcolor.red
87
+ columns = [
88
+ f'{data.a:,}',
89
+ f'{data.b:,}',
90
+ f'{data.v:,.3f}',
91
+ f'{data.discount * 100:+,.3f}%' if data.discount else '',
92
+ ]
93
+ table.add_column(
94
+ chr(65 + counter()),
95
+ [colorFunc(x) for x in columns],
96
+ )
97
+ print(table.get_string())
bcmd/tasks/mirror.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ from typing import Final
5
+
6
+ import typer
7
+ from beni import bcolor, bfile, bpath, btask
8
+ from beni.bfunc import syncCall
9
+
10
+ app: Final = btask.app
11
+
12
+
13
+ @app.command()
14
+ @syncCall
15
+ async def mirror(
16
+ disabled: bool = typer.Option(False, '--disabled', '-d', help="是否禁用"),
17
+ ):
18
+ '设置镜像'
19
+
20
+ # 根据不同的系统平台
21
+ match platform.system():
22
+ case 'Windows':
23
+ file = bpath.user('pip/pip.ini')
24
+ case 'Linux':
25
+ file = bpath.user('.pip/pip.conf')
26
+ case _:
27
+ btask.abort('暂时不支持该平台', platform.system())
28
+ return
29
+
30
+ if disabled:
31
+ bpath.remove(file)
32
+ bcolor.printRed('删除文件', file)
33
+ else:
34
+ content = _content.strip()
35
+ await bfile.writeText(file, content)
36
+ bcolor.printYellow(file)
37
+ bcolor.printMagenta(content)
38
+ bcolor.printGreen('OK')
39
+
40
+
41
+ # ------------------------------------------------------------------------------------
42
+
43
+ _content = '''
44
+ [global]
45
+ index-url = https://mirrors.aliyun.com/pypi/simple
46
+ '''
bcmd/tasks/pdf.py ADDED
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+ from typing import Final
3
+
4
+ import fitz
5
+ import typer
6
+ from beni import bcolor, bfile, btask
7
+ from beni.bfunc import syncCall
8
+
9
+ app: Final = btask.newSubApp('PDF相关')
10
+
11
+
12
+ @app.command()
13
+ @syncCall
14
+ async def output_images(
15
+ target: Path = typer.Option(Path.cwd(), '--target', help='PDF文件路径或多个PDF文件所在的目录'),
16
+ ):
17
+ '保存 PDF 文件里面的图片'
18
+
19
+ # 列出需要检查的PDF文件
20
+ pdfFileList: list[Path] = []
21
+ if target.is_dir():
22
+ pdfFileList = list(target.glob('*.pdf'))
23
+ elif target.is_file():
24
+ pdfFileList = [target]
25
+ else:
26
+ raise ValueError("目标路径不存在")
27
+
28
+ for pdf_file in pdfFileList:
29
+ pdf_document = fitz.open(pdf_file)
30
+ output_dir = pdf_file.with_name(pdf_file.stem + '-PDF图片文件')
31
+ for page_num in range(len(pdf_document)):
32
+ page = pdf_document.load_page(page_num)
33
+ images = page.get_images(full=True)
34
+ for img_index, img in enumerate(images):
35
+ xref = img[0]
36
+ base_image = pdf_document.extract_image(xref)
37
+ image_bytes = base_image["image"]
38
+ image_ext = base_image["ext"]
39
+ image_filename = f"page{page_num + 1}_img{img_index + 1}.{image_ext}"
40
+ image_path = output_dir / image_filename
41
+ output_dir.mkdir(exist_ok=True)
42
+ bcolor.printGreen(image_path)
43
+ await bfile.writeBytes(image_path, image_bytes)