Files
nl-tools/ui/image_preview.py
2025-12-11 09:47:00 +08:00

380 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
图片预览组件
- 原图/处理后对比预览
- 缩放/平移
- 信息显示
"""
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"