初始化
This commit is contained in:
379
ui/image_preview.py
Normal file
379
ui/image_preview.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
图片预览组件
|
||||
- 原图/处理后对比预览
|
||||
- 缩放/平移
|
||||
- 信息显示
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QSplitter, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QSize
|
||||
from PySide6.QtGui import QPixmap, QImage, QFont
|
||||
|
||||
from core.config import config
|
||||
|
||||
|
||||
class ImagePreviewWidget(QFrame):
|
||||
"""单个图片预览组件"""
|
||||
|
||||
def __init__(self, title: str = "预览", parent=None):
|
||||
super().__init__(parent)
|
||||
self.title = title
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setStyleSheet("""
|
||||
ImagePreviewWidget {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# 标题栏
|
||||
header = QHBoxLayout()
|
||||
|
||||
self.title_label = QLabel(self.title)
|
||||
self.title_label.setStyleSheet("""
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
""")
|
||||
header.addWidget(self.title_label)
|
||||
header.addStretch()
|
||||
|
||||
# 尺寸信息
|
||||
self.size_label = QLabel("")
|
||||
self.size_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
header.addWidget(self.size_label)
|
||||
|
||||
layout.addLayout(header)
|
||||
|
||||
# 图片显示区域
|
||||
self.image_container = QFrame()
|
||||
self.image_container.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.image_container.setMinimumHeight(250)
|
||||
|
||||
container_layout = QVBoxLayout(self.image_container)
|
||||
container_layout.setContentsMargins(8, 8, 8, 8)
|
||||
container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 占位图标
|
||||
self.placeholder_icon = QLabel("🖼️")
|
||||
self.placeholder_icon.setFont(QFont("Segoe UI Emoji", 40))
|
||||
self.placeholder_icon.setStyleSheet("color: #334155;")
|
||||
self.placeholder_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_icon)
|
||||
|
||||
# 占位文字
|
||||
self.placeholder_text = QLabel("暂无图片")
|
||||
self.placeholder_text.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
self.placeholder_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_text)
|
||||
|
||||
# 图片标签
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_label.setVisible(False)
|
||||
container_layout.addWidget(self.image_label)
|
||||
|
||||
layout.addWidget(self.image_container, 1)
|
||||
|
||||
# 文件信息
|
||||
self.info_label = QLabel("")
|
||||
self.info_label.setStyleSheet("color: #94a3b8; font-size: 11px;")
|
||||
self.info_label.setWordWrap(True)
|
||||
layout.addWidget(self.info_label)
|
||||
|
||||
def set_image(self, pixmap: QPixmap = None, file_path: str = None,
|
||||
pil_image: Image.Image = None, image_bytes: bytes = None):
|
||||
"""设置预览图片"""
|
||||
if pixmap:
|
||||
self._pixmap = pixmap
|
||||
elif file_path and os.path.exists(file_path):
|
||||
self._pixmap = QPixmap(file_path)
|
||||
self._image_info = self._get_file_info(file_path)
|
||||
elif pil_image:
|
||||
self._pixmap = self._pil_to_pixmap(pil_image)
|
||||
elif image_bytes:
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(image_bytes)
|
||||
else:
|
||||
self._pixmap = None
|
||||
|
||||
self._update_display()
|
||||
|
||||
def set_image_from_bytes(self, data: bytes, info: dict = None):
|
||||
"""从字节数据设置图片"""
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(data)
|
||||
if info:
|
||||
self._image_info = info
|
||||
self._update_display()
|
||||
|
||||
def _pil_to_pixmap(self, pil_image: Image.Image) -> QPixmap:
|
||||
"""PIL Image转QPixmap"""
|
||||
if pil_image.mode == "RGB":
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
elif pil_image.mode == "RGBA":
|
||||
data = pil_image.tobytes("raw", "RGBA")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 4, QImage.Format.Format_RGBA8888)
|
||||
else:
|
||||
pil_image = pil_image.convert("RGB")
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
return QPixmap.fromImage(qimage)
|
||||
|
||||
def _get_file_info(self, file_path: str) -> dict:
|
||||
"""获取文件信息"""
|
||||
try:
|
||||
size = os.path.getsize(file_path)
|
||||
with Image.open(file_path) as img:
|
||||
return {
|
||||
"path": file_path,
|
||||
"name": Path(file_path).name,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"size": size,
|
||||
"format": img.format
|
||||
}
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _update_display(self):
|
||||
"""更新显示"""
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
# 隐藏占位符
|
||||
self.placeholder_icon.setVisible(False)
|
||||
self.placeholder_text.setVisible(False)
|
||||
self.image_label.setVisible(True)
|
||||
|
||||
# 缩放图片适应容器
|
||||
container_size = self.image_container.size()
|
||||
max_width = container_size.width() - 20
|
||||
max_height = container_size.height() - 20
|
||||
|
||||
scaled = self._pixmap.scaled(
|
||||
QSize(max_width, max_height),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.image_label.setPixmap(scaled)
|
||||
|
||||
# 更新尺寸信息
|
||||
self.size_label.setText(f"{self._pixmap.width()}×{self._pixmap.height()}")
|
||||
|
||||
# 更新文件信息
|
||||
if self._image_info:
|
||||
size_str = self._format_size(self._image_info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {self._image_info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
else:
|
||||
# 显示占位符
|
||||
self.placeholder_icon.setVisible(True)
|
||||
self.placeholder_text.setVisible(True)
|
||||
self.image_label.setVisible(False)
|
||||
self.size_label.setText("")
|
||||
self.info_label.setText("")
|
||||
|
||||
def set_info(self, info: dict):
|
||||
"""设置图片信息"""
|
||||
self._image_info = info
|
||||
if info:
|
||||
size_str = self._format_size(info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self._update_display()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""窗口大小变化时重新缩放图片"""
|
||||
super().resizeEvent(event)
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
self._update_display()
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class DualPreviewWidget(QWidget):
|
||||
"""双栏预览组件 - 原图/处理后对比"""
|
||||
|
||||
save_requested = Signal(object) # 发出保存请求信号,携带处理后的数据
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._processed_data = None # 保存处理后的数据
|
||||
self._output_filename = ""
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 预览区 - 左右分栏
|
||||
preview_layout = QHBoxLayout()
|
||||
preview_layout.setSpacing(16)
|
||||
|
||||
# 原图预览
|
||||
self.original_preview = ImagePreviewWidget("📷 原图")
|
||||
preview_layout.addWidget(self.original_preview, 1)
|
||||
|
||||
# 处理后预览
|
||||
self.result_preview = ImagePreviewWidget("✨ 处理结果")
|
||||
preview_layout.addWidget(self.result_preview, 1)
|
||||
|
||||
layout.addLayout(preview_layout, 1)
|
||||
|
||||
# 对比信息
|
||||
self.compare_label = QLabel("")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.compare_label.setVisible(False)
|
||||
layout.addWidget(self.compare_label)
|
||||
|
||||
# 操作按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
self.save_btn = QPushButton("💾 保存结果")
|
||||
self.save_btn.setObjectName("primary_btn")
|
||||
self.save_btn.setMinimumSize(150, 40)
|
||||
self.save_btn.clicked.connect(self._on_save_clicked)
|
||||
self.save_btn.setEnabled(False)
|
||||
btn_layout.addWidget(self.save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def set_original(self, file_path: str):
|
||||
"""设置原图"""
|
||||
self.original_preview.set_image(file_path=file_path)
|
||||
|
||||
def set_result(self, data: bytes, info: dict = None, filename: str = "",
|
||||
show_size_compare: bool = True):
|
||||
"""设置处理结果"""
|
||||
self._processed_data = data
|
||||
self._output_filename = filename
|
||||
self.result_preview.set_image_from_bytes(data, info)
|
||||
self.save_btn.setEnabled(True)
|
||||
|
||||
# 显示对比信息(仅在压缩场景下显示大小对比)
|
||||
if info and show_size_compare:
|
||||
original_size = self.original_preview._image_info.get("size", 0)
|
||||
result_size = info.get("size", len(data))
|
||||
if original_size > 0:
|
||||
saved = original_size - result_size
|
||||
percent = (saved / original_size) * 100
|
||||
if saved > 0:
|
||||
self.compare_label.setText(
|
||||
f"✅ 处理完成! 节省 {self._format_size(saved)} ({percent:.1f}%)"
|
||||
)
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
else:
|
||||
# 文件变大或相同,不显示对比(可能是添加水印等操作)
|
||||
self.compare_label.setVisible(False)
|
||||
elif not show_size_compare:
|
||||
# 不需要显示大小对比(如水印、格式转换)
|
||||
self.compare_label.setText("✅ 处理完成!")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
|
||||
def _on_save_clicked(self):
|
||||
"""保存按钮点击"""
|
||||
if not self._processed_data:
|
||||
return
|
||||
|
||||
# 检查是否有默认保存路径
|
||||
default_dir = config.get_output_directory()
|
||||
auto_save = config.get("auto_save_to_default", False)
|
||||
|
||||
if default_dir and auto_save:
|
||||
# 自动保存
|
||||
save_path = os.path.join(default_dir, self._output_filename)
|
||||
else:
|
||||
# 询问保存位置
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存文件",
|
||||
os.path.join(default_dir, self._output_filename) if default_dir else self._output_filename,
|
||||
"图片文件 (*.jpg *.png *.webp)"
|
||||
)
|
||||
|
||||
if save_path:
|
||||
try:
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(self._processed_data)
|
||||
QMessageBox.information(self, "成功", f"文件已保存到:\n{save_path}")
|
||||
self.save_requested.emit(save_path)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"保存失败:\n{e}")
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self.original_preview.clear()
|
||||
self.result_preview.clear()
|
||||
self._processed_data = None
|
||||
self._output_filename = ""
|
||||
self.save_btn.setEnabled(False)
|
||||
self.compare_label.setVisible(False)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
Reference in New Issue
Block a user