[Python] 纯文本查看 复制代码
import sys
import os
from PIL import Image
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFileDialog, QListWidget, QProgressBar,
QMessageBox, QAction, QStatusBar, QGroupBox, QFrame)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QMimeData
from PyQt5.QtGui import QIcon, QFont, QPixmap, QColor, QPalette, QDragEnterEvent, QDropEvent
class ConversionThread(QThread):
"""后台线程用于执行图像转换任务"""
progress_updated = pyqtSignal(int, str)
conversion_finished = pyqtSignal()
error_occurred = pyqtSignal(str)
def __init__(self, file_paths, output_dir=None, background_color=(255, 255, 255)):
super().__init__()
self.file_paths = file_paths
self.output_dir = output_dir
self.background_color = background_color
def run(self):
total = len(self.file_paths)
for i, file_path in enumerate(self.file_paths):
try:
# 更新进度
self.progress_updated.emit(int((i + 1) / total * 100),
f"正在转换: {os.path.basename(file_path)}")
# 处理单个文件
self.convert_webp_to_gif(file_path)
except Exception as e:
self.error_occurred.emit(f"转换失败: {os.path.basename(file_path)} - {str(e)}")
self.conversion_finished.emit()
def convert_webp_to_gif(self, file_path):
"""转换单个WebP文件到GIF"""
# 打开WebP图像
img = Image.open(file_path)
# 确定输出路径
if self.output_dir:
output_path = os.path.join(
self.output_dir,
f"{os.path.splitext(os.path.basename(file_path))[0]}.gif"
)
else:
output_path = os.path.splitext(file_path)[0] + ".gif"
# 保存为GIF
self.save_as_gif(img, output_path)
# 关闭图像
img.close()
def save_as_gif(self, img, output_path):
"""将PIL Image保存为GIF"""
# 准备帧列表
frames = []
durations = []
# 检测是否是动画
if hasattr(img, 'is_animated') and img.is_animated:
frame_count = img.n_frames
else:
frame_count = 1
# 处理每一帧
for frame_index in range(frame_count):
try:
if frame_count > 1:
img.seek(frame_index)
# 获取当前帧
current_frame = img.copy()
# 获取帧持续时间(毫秒)
duration = img.info.get('duration', 100)
durations.append(duration)
# 处理透明度 - 使用更可靠的方法
frame_rgb = self.process_transparency(current_frame)
frames.append(frame_rgb)
except EOFError:
# 帧索引超出范围,停止处理
break
except Exception as e:
# 处理单帧错误,继续处理其他帧
print(f"处理第{frame_index}帧时出错: {e}")
continue
# 确保至少有1帧
if not frames:
raise ValueError("没有有效的帧可以保存")
# 保存为GIF
if len(frames) == 1:
# 单帧GIF
frames[0].save(
output_path,
format="GIF",
save_all=False,
optimize=True,
quality=85
)
else:
# 多帧GIF
frames[0].save(
output_path,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=durations,
loop=0, # 无限循环
optimize=True,
disposal=2, # 恢复到背景色
quality=85
)
def process_transparency(self, frame):
"""处理帧的透明度,确保返回RGB模式的图像"""
# 转换为RGBA模式以便处理透明度
if frame.mode in ['RGBA', 'LA', 'P', 'PA']:
# 转换为RGBA
rgba_frame = frame.convert('RGBA')
# 创建白色背景
background = Image.new('RGBA', rgba_frame.size,
(*self.background_color, 255))
# 合并:将透明图像放在白色背景上
alpha = rgba_frame.split()[3] # Alpha通道
rgb_frame = Image.composite(rgba_frame, background, alpha)
# 转换为RGB
rgb_frame = rgb_frame.convert('RGB')
elif frame.mode in ['RGB', 'L']:
# 已经是RGB或灰度,直接转换
rgb_frame = frame.convert('RGB')
else:
# 其他模式尝试转换为RGB
try:
rgb_frame = frame.convert('RGB')
except:
# 如果转换失败,先转换为RGBA再处理
rgba_frame = frame.convert('RGBA')
background = Image.new('RGBA', rgba_frame.size,
(*self.background_color, 255))
alpha = rgba_frame.split()[3]
rgb_frame = Image.composite(rgba_frame, background, alpha)
rgb_frame = rgb_frame.convert('RGB')
return rgb_frame
class WebPtoGifConverter(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("WebP 转 GIF 转换器")
self.setGeometry(100, 100, 850, 650)
# 设置应用图标
self.setWindowIcon(self.create_app_icon())
# 创建主部件和布局
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(15)
# 标题标签
title_label = QLabel("WebP 转 GIF 转换器")
title_font = QFont()
title_font.setPointSize(24)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #3498db; margin-bottom: 10px;")
main_layout.addWidget(title_label)
# 说明标签
description_label = QLabel("支持拖放文件/文件夹 | 支持批量转换")
description_label.setAlignment(Qt.AlignCenter)
description_label.setStyleSheet("color: #7f8c8d; margin-bottom: 20px; font-size: 14px;")
main_layout.addWidget(description_label)
# 创建操作区域
group_box = QGroupBox("操作面板")
group_box.setStyleSheet("""
QGroupBox {
font-weight: bold;
font-size: 14px;
border: 2px solid #3498db;
border-radius: 8px;
margin-top: 20px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 10px;
color: #3498db;
}
""")
main_layout.addWidget(group_box)
button_layout = QHBoxLayout()
button_layout.setContentsMargins(15, 20, 15, 20)
button_layout.setSpacing(20)
# 选择文件按钮
self.select_file_btn = QPushButton("选择文件")
self.select_file_btn.setIcon(QIcon.fromTheme("document-open"))
self.select_file_btn.setIconSize(QSize(20, 20))
self.select_file_btn.setStyleSheet(self.get_button_style("#3498db"))
self.select_file_btn.clicked.connect(self.select_files)
button_layout.addWidget(self.select_file_btn)
# 选择文件夹按钮
self.select_folder_btn = QPushButton("选择文件夹")
self.select_folder_btn.setIcon(QIcon.fromTheme("folder-open"))
self.select_folder_btn.setIconSize(QSize(20, 20))
self.select_folder_btn.setStyleSheet(self.get_button_style("#2ecc71"))
self.select_folder_btn.clicked.connect(self.select_folder)
button_layout.addWidget(self.select_folder_btn)
# 清空列表按钮
self.clear_list_btn = QPushButton("清空列表")
self.clear_list_btn.setIcon(QIcon.fromTheme("edit-clear"))
self.clear_list_btn.setIconSize(QSize(20, 20))
self.clear_list_btn.setStyleSheet(self.get_button_style("#e74c3c"))
self.clear_list_btn.clicked.connect(self.clear_file_list)
button_layout.addWidget(self.clear_list_btn)
# 转换按钮
self.convert_btn = QPushButton("开始转换")
self.convert_btn.setIcon(QIcon.fromTheme("media-playback-start"))
self.convert_btn.setIconSize(QSize(20, 20))
self.convert_btn.setStyleSheet(self.get_button_style("#9b59b6"))
self.convert_btn.clicked.connect(self.start_conversion)
button_layout.addWidget(self.convert_btn)
group_box.setLayout(button_layout)
# 文件列表区域
file_group = QGroupBox("待转换文件列表")
file_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
font-size: 14px;
border: 2px solid #2c3e50;
border-radius: 8px;
margin-top: 10px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 10px;
color: #2c3e50;
}
""")
main_layout.addWidget(file_group, 1)
file_layout = QVBoxLayout()
file_layout.setContentsMargins(10, 20, 10, 10)
# 文件列表
self.file_list = QListWidget()
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
self.file_list.setStyleSheet("""
QListWidget {
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 5px;
padding: 5px;
font-size: 13px;
}
QListWidget::item {
padding: 10px;
border-bottom: 1px solid #e9ecef;
}
QListWidget::item:selected {
background-color: #d6eaf8;
color: #1a73e8;
border-radius: 3px;
}
""")
self.file_list.setAlternatingRowColors(True)
# 设置拖放支持
self.file_list.setAcceptDrops(True)
self.file_list.setDragEnabled(True)
self.file_list.setDragDropMode(QListWidget.DropOnly)
self.file_list.viewport().setAcceptDrops(True)
self.file_list.setDropIndicatorShown(True)
file_layout.addWidget(self.file_list)
# 添加拖放提示标签
drop_hint = QLabel("拖放 WebP 文件或文件夹到这里")
drop_hint.setAlignment(Qt.AlignCenter)
drop_hint.setStyleSheet("color: #95a5a6; font-style: italic; font-size: 12px; padding: 10px;")
file_layout.addWidget(drop_hint)
file_group.setLayout(file_layout)
# 进度区域
progress_frame = QFrame()
progress_frame.setStyleSheet("background-color: #f1f2f6; border-radius: 8px; padding: 15px;")
main_layout.addWidget(progress_frame)
progress_layout = QVBoxLayout(progress_frame)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #ced4da;
border-radius: 5px;
text-align: center;
background-color: white;
height: 25px;
font-size: 12px;
}
QProgressBar::chunk {
background-color: #3498db;
width: 10px;
border-radius: 4px;
}
""")
self.progress_bar.setVisible(False)
progress_layout.addWidget(self.progress_bar)
# 状态标签
self.status_label = QLabel("就绪")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #7f8c8d; font-size: 13px; padding-top: 8px;")
progress_layout.addWidget(self.status_label)
# 状态栏
self.status_bar = QStatusBar()
self.status_bar.setStyleSheet("background-color: #ecf0f1; color: #7f8c8d; font-size: 11px;")
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪 | 拖放文件或文件夹到列表中")
# 创建菜单
self.create_menu()
# 存储文件路径
self.selected_files = []
# 设置初始状态
self.update_ui_state()
# 启用拖放
self.setAcceptDrops(True)
def create_app_icon(self):
"""创建应用图标"""
# 这里使用内置图标作为示例,实际应用中可以使用自定义图标
return QIcon.fromTheme("image-x-generic")
def get_button_style(self, color):
"""返回按钮样式表"""
return f"""
QPushButton {{
background-color: {color};
color: white;
border: none;
border-radius: 5px;
padding: 12px 20px;
font-weight: bold;
font-size: 13px;
min-width: 100px;
}}
QPushButton:hover {{
background-color: {self.adjust_color(color, -30)};
}}
QPushButton:pressed {{
background-color: {self.adjust_color(color, -50)};
padding: 13px 19px 11px 21px;
}}
QPushButton:disabled {{
background-color: #bdc3c7;
color: #7f8c8d;
}}
"""
def adjust_color(self, color, amount):
"""调整颜色亮度"""
# 将十六进制颜色转换为RGB
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
# 调整RGB值
r = max(0, min(255, r + amount))
g = max(0, min(255, g + amount))
b = max(0, min(255, b + amount))
# 转换回十六进制
return f"#{r:02x}{g:02x}{b:02x}"
def create_menu(self):
"""创建菜单栏"""
menubar = self.menuBar()
menubar.setStyleSheet("""
QMenuBar {
background-color: #f8f9fa;
padding: 4px;
border-bottom: 1px solid #e0e0e0;
}
QMenuBar::item {
padding: 5px 10px;
background: transparent;
border-radius: 4px;
}
QMenuBar::item:selected {
background: #e3f2fd;
}
QMenuBar::item:pressed {
background: #bbdefb;
}
""")
# 文件菜单
file_menu = menubar.addMenu("文件")
open_file_action = QAction("打开文件", self)
open_file_action.setShortcut("Ctrl+O")
open_file_action.triggered.connect(self.select_files)
file_menu.addAction(open_file_action)
open_folder_action = QAction("打开文件夹", self)
open_folder_action.setShortcut("Ctrl+D")
open_folder_action.triggered.connect(self.select_folder)
file_menu.addAction(open_folder_action)
file_menu.addSeparator()
exit_action = QAction("退出", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 编辑菜单
edit_menu = menubar.addMenu("编辑")
clear_action = QAction("清空列表", self)
clear_action.setShortcut("Ctrl+Del")
clear_action.triggered.connect(self.clear_file_list)
edit_menu.addAction(clear_action)
# 帮助菜单
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.setShortcut("F1")
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def dragEnterEvent(self, event: QDragEnterEvent):
"""处理拖拽进入事件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
"""处理拖放事件"""
urls = event.mimeData().urls()
if urls:
new_files = []
for url in urls:
path = url.toLocalFile()
# 如果是目录,则遍历
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
for file in files:
if file.lower().endswith('.webp'):
new_files.append(os.path.join(root, file))
# 如果是文件,直接添加
elif path.lower().endswith('.webp'):
new_files.append(path)
if new_files:
self.selected_files.extend(new_files)
self.update_file_list()
self.status_bar.showMessage(f"添加了 {len(new_files)} 个文件")
else:
self.status_bar.showMessage("未找到有效的 WebP 文件")
event.acceptProposedAction()
def select_files(self):
"""选择单个或多个文件"""
files, _ = QFileDialog.getOpenFileNames(
self, "选择WebP文件", "",
"WebP图像 (*.webp);;所有文件 (*)"
)
if files:
self.selected_files.extend(files)
self.update_file_list()
self.status_bar.showMessage(f"添加了 {len(files)} 个文件")
def select_folder(self):
"""选择文件夹并添加所有WebP文件"""
folder = QFileDialog.getExistingDirectory(
self, "选择包含WebP文件的文件夹"
)
if folder:
new_files = []
# 查找文件夹中的所有WebP文件
for root, dirs, files in os.walk(folder):
for file in files:
if file.lower().endswith(".webp"):
new_files.append(os.path.join(root, file))
if new_files:
self.selected_files.extend(new_files)
self.update_file_list()
self.status_bar.showMessage(f"添加了 {len(new_files)} 个文件")
else:
self.status_bar.showMessage("该文件夹中未找到 WebP 文件")
def clear_file_list(self):
"""清空文件列表"""
self.selected_files = []
self.file_list.clear()
self.update_ui_state()
self.status_bar.showMessage("已清空文件列表")
def update_file_list(self):
"""更新文件列表显示"""
self.file_list.clear()
for file_path in self.selected_files:
self.file_list.addItem(os.path.basename(file_path))
self.update_ui_state()
# 更新状态栏
count = len(self.selected_files)
self.status_bar.showMessage(f"已选择 {count} 个文件 | 拖放添加更多文件")
def update_ui_state(self):
"""根据当前状态更新UI元素"""
has_files = bool(self.selected_files)
self.convert_btn.setEnabled(has_files)
self.clear_list_btn.setEnabled(has_files)
if has_files:
self.convert_btn.setToolTip(f"转换 {len(self.selected_files)} 个文件")
else:
self.convert_btn.setToolTip("请先选择文件")
def start_conversion(self):
"""开始转换过程"""
if not self.selected_files:
QMessageBox.warning(self, "没有文件", "请先选择要转换的文件!")
return
# 询问输出目录
output_dir = QFileDialog.getExistingDirectory(
self, "选择输出目录",
options=QFileDialog.ShowDirsOnly
)
if not output_dir:
return
# 准备UI进行转换
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText("正在准备转换...")
self.status_bar.showMessage("转换中...")
# 禁用按钮
self.set_buttons_enabled(False)
# 创建并启动转换线程
self.conversion_thread = ConversionThread(self.selected_files, output_dir)
self.conversion_thread.progress_updated.connect(self.update_progress)
self.conversion_thread.conversion_finished.connect(self.conversion_complete)
self.conversion_thread.error_occurred.connect(self.show_error)
self.conversion_thread.start()
def set_buttons_enabled(self, enabled):
"""启用或禁用所有按钮"""
self.select_file_btn.setEnabled(enabled)
self.select_folder_btn.setEnabled(enabled)
self.clear_list_btn.setEnabled(enabled)
self.convert_btn.setEnabled(enabled)
def update_progress(self, value, message):
"""更新进度条和状态标签"""
self.progress_bar.setValue(value)
self.status_label.setText(message)
self.status_bar.showMessage(f"转换中: {value}%")
def conversion_complete(self):
"""转换完成时调用"""
self.progress_bar.setValue(100)
self.status_label.setText("转换完成!")
self.status_bar.showMessage(f"成功转换 {len(self.selected_files)} 个文件")
# 显示完成消息
QMessageBox.information(
self, "转换完成",
f"成功转换 {len(self.selected_files)} 个文件!\n\n"
f"输出目录: {self.conversion_thread.output_dir}"
)
# 重置UI
self.set_buttons_enabled(True)
self.selected_files = []
self.update_file_list()
# 隐藏进度条
self.progress_bar.setVisible(False)
def show_error(self, message):
"""显示错误消息"""
self.status_bar.showMessage(message)
QMessageBox.critical(self, "转换错误", message)
def show_about(self):
"""显示关于对话框"""
about_text = """
<html>
<head>
<style>
h2 { color: #3498db; text-align: center; }
p { font-size: 13px; }
.center { text-align: center; }
.highlight { color: #e74c3c; font-weight: bold; }
</style>
</head>
<body>
<h2>WebP 转 GIF 转换器</h2>
<p class="center">版本: 2.0</p>
<p>此工具可将WebP图像转换为GIF格式,支持批量转换和拖放操作。</p>
<p><b>主要功能:</b></p>
<ul>
<li>拖放文件或文件夹添加文件</li>
<li>批量转换多个文件</li>
<li>转换进度实时显示</li>
<li>支持选择输出目录</li>
</ul>
<p class="center highlight">使用方法: 拖放WebP文件到窗口或使用按钮添加文件</p>
<p class="center">© 2023 WebP转GIF工具 | 使用Python和PyQt5开发</p>
</body>
</html>
"""
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("关于")
msg.setTextFormat(Qt.RichText)
msg.setText(about_text)
msg.setStandardButtons(QMessageBox.Ok)
msg.exec_()
if __name__ == "__main__":
app = QApplication(sys.argv)
# 设置应用样式
app.setStyle("Fusion")
# 设置全局字体
font = QFont()
font.setFamily("Segoe UI")
font.setPointSize(10)
app.setFont(font)
# 设置调色板
palette = QPalette()
palette.setColor(QPalette.Window, QColor(245, 245, 245))
palette.setColor(QPalette.WindowText, Qt.black)
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240))
palette.setColor(QPalette.ToolTipBase, Qt.white)
palette.setColor(QPalette.ToolTipText, Qt.black)
palette.setColor(QPalette.Text, Qt.black)
palette.setColor(QPalette.Button, QColor(240, 240, 240))
palette.setColor(QPalette.ButtonText, Qt.black)
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Highlight, QColor(52, 152, 219))
palette.setColor(QPalette.HighlightedText, Qt.white)
app.setPalette(palette)
converter = WebPtoGifConverter()
converter.show()
sys.exit(app.exec_())