first commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
python -m nuitka \
|
||||
--standalone \
|
||||
--onefile \
|
||||
--follow-imports \
|
||||
--lto=yes \
|
||||
--jobs=8 \
|
||||
--remove-output \
|
||||
--output-dir=dist \
|
||||
--enable-plugin=numpy \
|
||||
--include-package=pymysql \
|
||||
--include-module=sqlalchemy.dialects.mysql \
|
||||
--include-module=sqlalchemy.engine \
|
||||
--include-module=sqlalchemy.sql \
|
||||
--include-module=cv2 \
|
||||
--noinclude-custom-mode=cv2.gui:warning \
|
||||
--nofollow-import-to=matplotlib,scipy,pandas,tkinter,test \
|
||||
--noinclude-default-mode=error \
|
||||
--python-flag=no_docstrings \
|
||||
--noinclude-data-files=*.png,*.mp4,*.xls* \
|
||||
main.py
|
||||
@@ -0,0 +1,367 @@
|
||||
import configparser
|
||||
import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
from build import get_settings
|
||||
|
||||
def download_file(url, dest):
|
||||
"""下载文件到指定路径"""
|
||||
try:
|
||||
r = requests.get(url, stream=True)
|
||||
r.raise_for_status()
|
||||
with open(dest, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"下载失败: {e}")
|
||||
return False
|
||||
|
||||
def load_config(config_path):
|
||||
"""加载配置文件"""
|
||||
return get_settings()
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
# 指定 UTF-8 编码读取配置文件
|
||||
config.read(config_path, encoding='utf-8')
|
||||
if not config.sections(): # 检查是否成功读取到配置项
|
||||
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
|
||||
return config
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"配置文件 {config_path} 不存在")
|
||||
except UnicodeDecodeError:
|
||||
# 尝试其他常见编码
|
||||
try:
|
||||
config.read(config_path, encoding='gbk')
|
||||
if not config.sections():
|
||||
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
|
||||
return config
|
||||
except Exception as e:
|
||||
raise ValueError(f"配置文件 {config_path} 编码错误: {str(e)}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"加载配置文件 {config_path} 失败: {str(e)}")
|
||||
|
||||
def build_nuitka_command(config, args, app_name):
|
||||
"""构造Nuitka编译命令"""
|
||||
cmd = []
|
||||
|
||||
# 基本选项
|
||||
|
||||
if compiler := args.compiler or config.nuitka.compiler:
|
||||
cmd.append(f"--{compiler}")
|
||||
if args.show_progress or config.nuitka.show_progress:
|
||||
cmd.append("--show-progress")
|
||||
if args.standalone or config.nuitka.standalone:
|
||||
cmd.append("--standalone")
|
||||
if args.onefile or config.nuitka.onefile:
|
||||
cmd.append("--onefile")
|
||||
if config.nuitka.remove_output:
|
||||
cmd.append("--remove-output")
|
||||
if args.disable_console or config.nuitka.disable_console:
|
||||
cmd.append("--windows-disable-console")
|
||||
|
||||
# 版本信息
|
||||
if product_version := config.version.version:
|
||||
cmd.append(f"--product-version={product_version}")
|
||||
if file_version := config.version.version_str:
|
||||
cmd.append(f"--file-version={file_version}")
|
||||
if company_name := config.version.author:
|
||||
cmd.append(f"--company-name={company_name}")
|
||||
if product_name := (config.version.alias or config.version.name):
|
||||
cmd.append(f"--product-name={product_name}")
|
||||
cmd.append(f"--output-filename={app_name}")
|
||||
|
||||
# 添加新的编译选项
|
||||
if config.nuitka.get('follow_imports', True):
|
||||
cmd.append("--follow-imports")
|
||||
|
||||
if lto := config.nuitka.lto:
|
||||
cmd.append(f"--lto={lto}")
|
||||
|
||||
if jobs := args.jobs or config.nuitka.jobs:
|
||||
cmd.append(f"--jobs={jobs}")
|
||||
|
||||
# 插件处理
|
||||
if plugins := config.nuitka.enable_plugin:
|
||||
for plugin in plugins.split(','):
|
||||
cmd.append(f"--enable-plugin={plugin.strip()}")
|
||||
|
||||
# 包和模块包含
|
||||
if packages := config.nuitka.include_package:
|
||||
for package in packages.split(','):
|
||||
cmd.append(f"--include-package={package.strip()}")
|
||||
|
||||
if modules := config.nuitka.include_module:
|
||||
for module in modules.split(','):
|
||||
cmd.append(f"--include-module={module.strip()}")
|
||||
|
||||
# 不跟随的导入
|
||||
if nofollow := config.nuitka.nofollow_import_to:
|
||||
for item in nofollow.split(','):
|
||||
cmd.append(f"--nofollow-import-to={item.strip()}")
|
||||
|
||||
# 数据文件排除
|
||||
if noinclude_data := config.nuitka.noinclude_data_files:
|
||||
for pattern in noinclude_data.split(','):
|
||||
cmd.append(f"--noinclude-data-files={pattern.strip()}")
|
||||
|
||||
# Python标志
|
||||
if python_flags := config.nuitka.python_flag:
|
||||
for flag in python_flags.split(','):
|
||||
cmd.append(f"--python-flag={flag.strip()}")
|
||||
|
||||
# 图标处理
|
||||
icon_source = args.icon or config.build.icon_file
|
||||
if icon_source:
|
||||
ico_path = 'favicon.ico'
|
||||
if 'ico' not in icon_source:
|
||||
from tranform_to_ico import convert_png_to_ico
|
||||
convert_png_to_ico(icon_source, ico_path, sizes=[(256, 256)], sharpen=False)
|
||||
else:
|
||||
ico_path = icon_source
|
||||
|
||||
if ico_path.startswith(('http://', 'https://')):
|
||||
local_icon = Path("download.ico")
|
||||
if download_file(ico_path, local_icon):
|
||||
cmd.append(f"--windows-icon-from-ico={local_icon}")
|
||||
else:
|
||||
cmd.append(f"--windows-icon-from-ico={ico_path}")
|
||||
|
||||
# 额外选项
|
||||
if extra_options := config.nuitka.extra_options:
|
||||
if isinstance(extra_options, str):
|
||||
extra_options = extra_options.split(',')
|
||||
cmd.extend(extra_options)
|
||||
|
||||
# 输出目录
|
||||
output_dir = args.output or config.build.output_dir or 'dist'
|
||||
cmd.append(f"--output-dir={output_dir}")
|
||||
|
||||
python_exe = sys.executable
|
||||
cmd = [python_exe, "-m", "nuitka"] + list(set(cmd))
|
||||
|
||||
# 源文件
|
||||
source_script = args.script or config.build.source_script or 'source_script'
|
||||
cmd.append(source_script)
|
||||
|
||||
return cmd
|
||||
|
||||
def sign_executable(config, args):
|
||||
"""签名可执行文件"""
|
||||
if not args.sign:
|
||||
print("跳过签名步骤")
|
||||
return True
|
||||
|
||||
sign_tool = config.signing.sign_tool
|
||||
if not sign_tool or not Path(sign_tool).exists():
|
||||
print("警告: signtool.exe 未找到,跳过签名")
|
||||
return False
|
||||
|
||||
cert_file = args.cert or config.signing.cert_file
|
||||
if not cert_file or not Path(cert_file).exists():
|
||||
print("警告: 证书文件不存在,跳过签名")
|
||||
return False
|
||||
|
||||
cert_password = args.password or config.signing.cert_password or ''
|
||||
timestamp_server = args.timestamp or config.signing.timestamp_server or 'http://timestamp.digicert.com'
|
||||
|
||||
output_dir = args.output or config.build.output_dirt
|
||||
source_script = args.script or config.build.source_script
|
||||
exe_name = Path(source_script).stem + ".exe"
|
||||
exe_path = Path(output_dir) / exe_name
|
||||
|
||||
sign_cmd = [
|
||||
sign_tool,
|
||||
"sign",
|
||||
"/tr", timestamp_server,
|
||||
"/td", "sha256",
|
||||
"/fd", "sha256",
|
||||
"/f", cert_file,
|
||||
"/p", cert_password,
|
||||
str(exe_path),
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(sign_cmd, check=True)
|
||||
print(f"✓ 签名成功: {exe_path}")
|
||||
|
||||
# 验证签名
|
||||
verify_cmd = [sign_tool, "verify", "/pa", str(exe_path)]
|
||||
subprocess.run(verify_cmd, check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ 签名失败: {e}")
|
||||
return False
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Python打包签名工具")
|
||||
parser.add_argument('--config', default='build_config.ini',
|
||||
help="配置文件路径 (默认: build_config.ini)")
|
||||
parser.add_argument('--script', help="覆盖配置中的源脚本")
|
||||
parser.add_argument('--output', help="覆盖输出目录")
|
||||
parser.add_argument('--icon', help="本地或在线ICO图标路径 (http/https)")
|
||||
parser.add_argument('--jobs', help="并行进程数")
|
||||
parser.add_argument('--compiler', default="mingw64", help="编译器类型")
|
||||
parser.add_argument('--show-progress', default="store_true", help="显示编译进度")
|
||||
parser.add_argument('--standalone', action='store_true', help="强制启用standalone模式")
|
||||
parser.add_argument('--onefile', action='store_true', help="强制启用单文件模式")
|
||||
parser.add_argument('--disable-console', action='store_true',
|
||||
help="禁用控制台窗口 (GUI程序)")
|
||||
parser.add_argument('--no-sign', dest='sign', action='store_false',
|
||||
help="跳过签名步骤")
|
||||
parser.add_argument('--cert', help="覆盖证书路径")
|
||||
parser.add_argument('--password', help="证书密码")
|
||||
parser.add_argument('--timestamp', help="覆盖时间戳服务器URL")
|
||||
return parser.parse_args()
|
||||
|
||||
def format_file_size(size_bytes):
|
||||
"""将文件大小转换为易读格式"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.2f} TB"
|
||||
|
||||
def get_exe_path(config, args):
|
||||
"""获取生成的exe文件路径"""
|
||||
output_dir = args.output or config.build.output_dirt or 'dist'
|
||||
source_script = args.script or config.build.source_script
|
||||
exe_name = Path(source_script).stem + (".exe" if os.name == 'nt' else "")
|
||||
return Path(output_dir) / exe_name
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
print("=== 开始构建 ===")
|
||||
start_time = time.time()
|
||||
build_success = True
|
||||
sign_success = False
|
||||
|
||||
try:
|
||||
config = load_config(args.config)
|
||||
except FileNotFoundError as e:
|
||||
print(f"错误: {e}")
|
||||
return
|
||||
|
||||
# import json; print(json.dumps(config, indent=4, ensure_ascii=False))
|
||||
# import json; print(json.dumps(get_settings(), indent=4, ensure_ascii=False))
|
||||
# return
|
||||
|
||||
# 清理旧构建
|
||||
output_dir = args.output or config.build.output_dir or 'dist'
|
||||
if config.build.clean and Path(output_dir).exists():
|
||||
try:
|
||||
shutil.rmtree(output_dir)
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"已清理输出目录: {output_dir}")
|
||||
except Exception as e:
|
||||
print(f"清理目录时出错: {e}")
|
||||
build_success = False
|
||||
|
||||
product_name = (config.version.alias or config.version.name)
|
||||
product_version = config.version.version
|
||||
app_name = f"{product_name}{'v'+product_version if product_version else ''}"
|
||||
print(f"应用名称: {app_name}")
|
||||
build_success = True
|
||||
|
||||
if build_success:
|
||||
# 编译
|
||||
print("\n[1/3] 正在使用Nuitka编译...")
|
||||
try:
|
||||
nuitka_cmd = build_nuitka_command(config, args, app_name)
|
||||
print("执行命令:", " ".join(nuitka_cmd))
|
||||
|
||||
compile_start = time.time()
|
||||
subprocess.run(nuitka_cmd, check=True)
|
||||
print(f"编译完成,耗时: {time.time() - compile_start:.2f}秒")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"编译失败: {e}")
|
||||
build_success = False
|
||||
|
||||
if build_success and args.sign:
|
||||
# 签名
|
||||
print("\n[2/3] 正在处理签名...")
|
||||
try:
|
||||
sign_start = time.time()
|
||||
sign_success = sign_executable(config, args)
|
||||
if sign_success:
|
||||
print(f"签名完成,耗时: {time.time() - sign_start:.2f}秒")
|
||||
except Exception as e:
|
||||
print(f"签名过程中出错: {e}")
|
||||
sign_success = False
|
||||
|
||||
if build_success:
|
||||
# 压缩文件
|
||||
print("\n[3/3] 正在压缩文件...")
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
zip_path = f"{output_dir}/{app_name}.zip"
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file in [f'{output_dir}/{app_name}.exe', *(config.build.resource_files or [])]:
|
||||
if not file: continue
|
||||
filename = Path(file).name
|
||||
zipf.write(file, arcname=filename)
|
||||
print(f"压缩完成: {zip_path}")
|
||||
except Exception as e:
|
||||
print(f"压缩文件时出错: {e}")
|
||||
build_success = False
|
||||
|
||||
zip_app_name = f"{config.version.name}{'v'+product_version if product_version else ''}.zip"
|
||||
version_data = config.version.to_dict()
|
||||
save_url = config.build.save_url.strip('/')
|
||||
access_url = config.version.access_url.strip('/')
|
||||
version_data['url'] = access_url + '/' + zip_app_name
|
||||
|
||||
import json
|
||||
with open(f'{output_dir}/version.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(dict(
|
||||
data=version_data,
|
||||
code=200,
|
||||
success='true',
|
||||
message=f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {app_name} 打包完成',
|
||||
), f, ensure_ascii=False, indent=4)
|
||||
print(f"版本信息已保存到: {output_dir}/version.json")
|
||||
|
||||
if not os.system(f'curl -T {zip_path} {save_url}/{zip_app_name}'):
|
||||
print(f"上传完成: {version_data['url']}")
|
||||
os.system(f'curl -T {output_dir}/version.json {save_url}/latest')
|
||||
|
||||
# 构建总结
|
||||
print("\n=== 构建总结 ===")
|
||||
total_time = time.time() - start_time
|
||||
status = "成功" if build_success else "失败"
|
||||
|
||||
summary = [
|
||||
f"构建状态: {status}",
|
||||
f"总耗时: {total_time:.2f}秒",
|
||||
f"输出目录: {output_dir}",
|
||||
]
|
||||
|
||||
if build_success:
|
||||
exe_path = get_exe_path(config, args)
|
||||
if exe_path.exists():
|
||||
file_time = time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(exe_path.stat().st_mtime))
|
||||
summary.extend([
|
||||
f"生成文件: {exe_path}",
|
||||
f"文件大小: {format_file_size(exe_path.stat().st_size)}",
|
||||
f"修改时间: {file_time}",
|
||||
])
|
||||
|
||||
if args.sign:
|
||||
summary.append(f"签名状态: {'成功' if sign_success else '失败'}")
|
||||
|
||||
print("\n".join(summary))
|
||||
|
||||
if not build_success:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
python scripts/generate_pyi.py image_loader.py
|
||||
@@ -0,0 +1,125 @@
|
||||
# generate_pyi.py
|
||||
|
||||
import re
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def sanitize_name(name: str) -> str:
|
||||
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
||||
return re.sub(r'^[^a-zA-Z_]+', '', name)
|
||||
|
||||
def pascal_case(s: str) -> str:
|
||||
"""将字符串转为 PascalCase"""
|
||||
return ''.join(word.capitalize() for word in re.split(r'[\W_]+', s) if word)
|
||||
|
||||
def generate_pyi(base_dir: str, output_file: str):
|
||||
base_path = Path(base_dir)
|
||||
if not base_path.exists():
|
||||
raise FileNotFoundError(f"资源目录不存在: {base_dir}")
|
||||
|
||||
# 构建目录树结构
|
||||
tree = {}
|
||||
|
||||
for root, dirs, files in os.walk(str(base_path)):
|
||||
relative_path = Path(root).relative_to(base_path)
|
||||
parts = list(relative_path.parts) if relative_path != Path('.') else []
|
||||
|
||||
# 插入当前路径到树中
|
||||
current = tree
|
||||
for part in parts:
|
||||
safe_part = sanitize_name(part)
|
||||
if safe_part not in current:
|
||||
current[safe_part] = {}
|
||||
current = current[safe_part]
|
||||
|
||||
# 添加图像文件作为叶子节点
|
||||
for file in files:
|
||||
if file.startswith('.'):
|
||||
continue
|
||||
if file.split('.')[-1].lower() in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}:
|
||||
stem = sanitize_name(Path(file).stem)
|
||||
current[stem] = None # 表示这是一个图像资源(str 类型)
|
||||
|
||||
# 构建类定义
|
||||
class_defs = []
|
||||
written_classes = set()
|
||||
|
||||
def build_class(class_name: str, data: dict):
|
||||
if not data or class_name in written_classes:
|
||||
return
|
||||
written_classes.add(class_name)
|
||||
|
||||
lines = [f"class {class_name}:\n"]
|
||||
has_content = False
|
||||
|
||||
for key in sorted(data.keys()):
|
||||
value = data[key]
|
||||
safe_key = key
|
||||
if value is None:
|
||||
# 图像文件:直接定义为 str 属性
|
||||
lines.append(f" {safe_key}: str\n")
|
||||
has_content = True
|
||||
else:
|
||||
# 子目录:引用一个类
|
||||
nested_class_name = pascal_case(safe_key)
|
||||
lines.append(f" {safe_key}: \"{nested_class_name}\"\n")
|
||||
build_class(nested_class_name, value)
|
||||
has_content = True
|
||||
|
||||
if has_content:
|
||||
class_defs.append("".join(lines))
|
||||
else:
|
||||
# 空类不写入
|
||||
written_classes.remove(class_name)
|
||||
|
||||
# 构建主类 ImageResourceLoader
|
||||
loader_lines = []
|
||||
for key in sorted(tree.keys()):
|
||||
value = tree[key]
|
||||
if not value: # 跳过空的子目录
|
||||
loader_lines.append(f" {key}: str\n")
|
||||
continue
|
||||
class_name = pascal_case(key)
|
||||
loader_lines.append(f" {key}: {class_name}\n")
|
||||
build_class(class_name, value)
|
||||
|
||||
# 写入 .pyi 文件
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("from typing import Optional, Any\n\n")
|
||||
|
||||
# 写入所有子类定义
|
||||
for line in class_defs:
|
||||
f.write(line + "\n")
|
||||
|
||||
# 写入主类
|
||||
f.write("class ImageResourceLoader:\n")
|
||||
f.write(" def __init__(self, base_dir: str): ...\n")
|
||||
f.write(" def list_resources(self) -> None: ...\n")
|
||||
f.write(" def get(self, path: str, default: Optional[str] = ...) -> Optional[str]: ...\n")
|
||||
f.write(" def __getattr__(self, name: str) -> Any: ...\n")
|
||||
f.write("\n # 自动生成的资源类属性(仅用于 IDE 提示)\n")
|
||||
f.writelines(loader_lines)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="为 ImageResourceLoader 类生成 .pyi 存根文件。")
|
||||
parser.add_argument("source_file", help="Python 源文件路径(例如:image_loader.py)")
|
||||
parser.add_argument("--base_dir", default=".automation",
|
||||
help="图像资源目录,默认值:.automation")
|
||||
args = parser.parse_args()
|
||||
|
||||
source_path = Path(args.source_file)
|
||||
if not source_path.exists():
|
||||
raise FileNotFoundError(f"源文件不存在: {args.source_file}")
|
||||
|
||||
# 输出 .pyi 文件路径
|
||||
pyi_path = source_path.with_suffix(".pyi")
|
||||
|
||||
print(f"生成存根文件: {pyi_path}")
|
||||
print(f"使用资源目录: {args.base_dir}")
|
||||
|
||||
generate_pyi(args.base_dir, str(pyi_path))
|
||||
"""
|
||||
python scripts/generate_pyi.py image_loader.py --base_dir assets/images
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,15 @@
|
||||
source scripts/venv.sh
|
||||
|
||||
cd app/utils/some_sdk
|
||||
git pull origin main
|
||||
|
||||
cd ../../../
|
||||
|
||||
export DB_CONNECTION=mysql
|
||||
export DB_HOST=127.0.0.1
|
||||
export DB_PORT=3306
|
||||
export DB_NAME=my_db
|
||||
export DB_USER=root
|
||||
export DB_PASSWORD=xxxxxx
|
||||
|
||||
python run.py
|
||||
@@ -0,0 +1,74 @@
|
||||
from PIL import Image, ImageEnhance, ImageFilter
|
||||
|
||||
def convert_png_to_ico(png_path, ico_path, sizes=None, resample=Image.LANCZOS, sharpen=False):
|
||||
"""
|
||||
将PNG图片转换为ICO图标文件,同时优化圆角效果
|
||||
|
||||
参数:
|
||||
- png_path: 输入PNG文件路径
|
||||
- ico_path: 输出ICO文件路径
|
||||
- sizes: 图标尺寸列表
|
||||
- resample: 重采样方法,默认为Image.LANCZOS
|
||||
- sharpen: 是否应用锐化滤镜
|
||||
"""
|
||||
if sizes is None:
|
||||
sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (256, 256)]
|
||||
|
||||
try:
|
||||
with Image.open(png_path) as img:
|
||||
# 确保图片为RGBA模式
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# 检查是否有透明通道
|
||||
has_transparency = False
|
||||
if img.mode == 'RGBA':
|
||||
alpha = img.getchannel('A')
|
||||
if alpha.getextrema()[0] < 255:
|
||||
has_transparency = True
|
||||
|
||||
# 为每个尺寸创建单独的图像
|
||||
icon_sizes = []
|
||||
for size in sizes:
|
||||
# 调整图片尺寸
|
||||
if size[0] < img.size[0] or size[1] < img.size[1]:
|
||||
# 缩小图片时使用高质量重采样算法
|
||||
resized_img = img.resize(size, resample=resample)
|
||||
else:
|
||||
# 放大图片时使用NEAREST算法避免模糊(适用于需要清晰边缘的情况)
|
||||
resized_img = img.resize(size, resample=Image.NEAREST)
|
||||
|
||||
# 应用锐化滤镜(可选)
|
||||
if sharpen:
|
||||
enhancer = ImageEnhance.Sharpness(resized_img)
|
||||
resized_img = enhancer.enhance(1.5) # 增强锐度
|
||||
|
||||
icon_sizes.append(resized_img)
|
||||
|
||||
# 保存为ICO文件
|
||||
if len(icon_sizes) > 0:
|
||||
# 使用第一个尺寸作为主图标
|
||||
icon_sizes[0].save(
|
||||
ico_path,
|
||||
format='ICO',
|
||||
sizes=[(img.size[0], img.size[1]) for img in icon_sizes]
|
||||
)
|
||||
print(f"成功将 {png_path} 转换为 {ico_path}")
|
||||
print(f"包含尺寸: {sizes}")
|
||||
if has_transparency:
|
||||
print("注意:已保留图片的透明通道(圆角效果)")
|
||||
else:
|
||||
print("图片没有检测到透明通道(可能没有圆角效果)")
|
||||
else:
|
||||
print("错误:未指定有效尺寸")
|
||||
|
||||
except Exception as e:
|
||||
print(f"转换失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 输入和输出文件路径
|
||||
png_path = 'app.png'
|
||||
ico_path = 'favicon.ico'
|
||||
|
||||
# 调用函数进行转换,禁用锐化处理
|
||||
convert_png_to_ico(png_path, ico_path, sizes=[(256, 256)], sharpen=False)
|
||||
@@ -0,0 +1,23 @@
|
||||
default_value=vue-fastapi-admin
|
||||
app=${1:-$default_value}
|
||||
platform=$(uname -s)
|
||||
|
||||
if [ "${platform}" == "Linux" ]; then
|
||||
env_path=/data/env/python/${app}
|
||||
bin_path=${env_path}/bin
|
||||
# 使用 [[ ]] 进行模式匹配
|
||||
elif [[ "${platform}" == *"MINGW64"* ]]; then
|
||||
env_path=/d/data/python-venv/${app}
|
||||
bin_path=${env_path}/Scripts
|
||||
fi
|
||||
|
||||
if [ -d "${env_path}" ]; then
|
||||
echo "Activating virtual environment..."
|
||||
source ${bin_path}/activate
|
||||
else
|
||||
echo "Creating virtual environment..."
|
||||
python -m venv ${env_path}
|
||||
echo "Activating virtual environment..."
|
||||
source ${bin_path}/activate
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
Reference in New Issue
Block a user