328 lines
11 KiB
Python
Executable file
328 lines
11 KiB
Python
Executable file
import sys
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QMainWindow, QLabel, QVBoxLayout, QHBoxLayout,
|
|
QWidget, QProgressBar, QMessageBox, QPushButton
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
from PyQt6.QtGui import QDragEnterEvent, QDropEvent
|
|
from PIL import Image, ImageOps
|
|
|
|
# Disable decompression bomb limit for massive wallpapers
|
|
Image.MAX_IMAGE_PIXELS = None
|
|
|
|
# Target resolutions
|
|
RESOLUTIONS = {
|
|
# Desktop Full HD & QHD & 4K
|
|
"1920x1080": (1920, 1080),
|
|
"2560x1440": (2560, 1440),
|
|
"3840x2160": (3840, 2160),
|
|
# Desktop Ultrawide
|
|
"2560x1080": (2560, 1080), # UW FHD
|
|
"3440x1440": (3440, 1440), # UW QHD
|
|
"5120x2160": (5120, 2160), # UW 4K
|
|
# Mobile
|
|
"1080x1920": (1080, 1920),
|
|
"1284x2778": (1284, 2778),
|
|
"1440x2560": (1440, 2560),
|
|
}
|
|
|
|
|
|
class ExporterWorker(QThread):
|
|
progress_updated = pyqtSignal(int)
|
|
status_updated = pyqtSignal(str)
|
|
finished_success = pyqtSignal(str)
|
|
finished_error = pyqtSignal(str)
|
|
|
|
def __init__(self, image_paths: list[Path]):
|
|
super().__init__()
|
|
self.image_paths = image_paths
|
|
|
|
def run(self):
|
|
try:
|
|
total_tasks = len(self.image_paths) * len(RESOLUTIONS)
|
|
completed = 0
|
|
|
|
output_dirs = []
|
|
|
|
for path in self.image_paths:
|
|
self.status_updated.emit(f"Loading {path.name}...")
|
|
original_image = Image.open(path)
|
|
|
|
# Convert to RGB if it's not (e.g. RGBA or P) to save cleanly as JPEG/PNG
|
|
# while keeping background black if there's transparency
|
|
if original_image.mode in ('RGBA', 'LA') or (original_image.mode == 'P' and 'transparency' in original_image.info):
|
|
bg = Image.new('RGB', original_image.size, (0, 0, 0))
|
|
if original_image.mode == 'P':
|
|
original_image = original_image.convert('RGBA')
|
|
bg.paste(original_image, mask=original_image.split()[3])
|
|
original_image = bg
|
|
elif original_image.mode != 'RGB':
|
|
original_image = original_image.convert('RGB')
|
|
|
|
original_w, original_h = original_image.size
|
|
|
|
# Create output directory
|
|
output_dir = path.parent / path.stem
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if str(output_dir) not in output_dirs:
|
|
output_dirs.append(str(output_dir))
|
|
|
|
for name, (w, h) in RESOLUTIONS.items():
|
|
self.status_updated.emit(f"Generating {name}...")
|
|
|
|
# Check if target is larger than original to avoid upscaling
|
|
if w > original_w or h > original_h:
|
|
pass # skipping
|
|
else:
|
|
resized = ImageOps.fit(original_image, (w, h), method=Image.Resampling.LANCZOS, centering=(0.5, 0.5))
|
|
output_path = output_dir / f"{path.stem}_{name}.jpg"
|
|
resized.save(output_path, "JPEG", quality=95)
|
|
|
|
completed += 1
|
|
self.progress_updated.emit(int((completed / total_tasks) * 100))
|
|
|
|
original_image.close()
|
|
|
|
self.status_updated.emit("Done!")
|
|
|
|
out_msg = "\n".join(output_dirs[:3])
|
|
if len(output_dirs) > 3:
|
|
out_msg += f"\n... and {len(output_dirs)-3} more directory(s)."
|
|
self.finished_success.emit(out_msg)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
self.finished_error.emit(str(e))
|
|
|
|
|
|
class DropZone(QLabel):
|
|
file_dropped = pyqtSignal(list)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.setText("\n\nDrop Image(s)\nHere\n\n")
|
|
self.setStyleSheet("""
|
|
QLabel {
|
|
border: 2px dashed #aaa;
|
|
border-radius: 8px;
|
|
background-color: #2a2a2a;
|
|
color: #ffffff;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
QLabel:hover {
|
|
border-color: #4a90e2;
|
|
background-color: #333333;
|
|
}
|
|
""")
|
|
self.setAcceptDrops(True)
|
|
|
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
|
if event.mimeData().hasUrls():
|
|
urls = event.mimeData().urls()
|
|
if urls:
|
|
has_image = any(Path(u.toLocalFile()).suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.bmp'] for u in urls)
|
|
if has_image:
|
|
event.acceptProposedAction()
|
|
self.setStyleSheet("""
|
|
QLabel {
|
|
border: 2px dashed #4a90e2;
|
|
border-radius: 8px;
|
|
background-color: #384252;
|
|
color: #ffffff;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
return
|
|
event.ignore()
|
|
|
|
def dragLeaveEvent(self, event):
|
|
self.setStyleSheet("""
|
|
QLabel {
|
|
border: 2px dashed #aaa;
|
|
border-radius: 8px;
|
|
background-color: #2a2a2a;
|
|
color: #ffffff;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
QLabel:hover {
|
|
border-color: #4a90e2;
|
|
background-color: #333333;
|
|
}
|
|
""")
|
|
|
|
def dropEvent(self, event: QDropEvent):
|
|
self.dragLeaveEvent(None) # reset style
|
|
paths = []
|
|
for url in event.mimeData().urls():
|
|
p = Path(url.toLocalFile())
|
|
if p.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.bmp']:
|
|
paths.append(p)
|
|
if paths:
|
|
self.file_dropped.emit(paths)
|
|
|
|
|
|
class TitleBar(QWidget):
|
|
def __init__(self, parent):
|
|
super().__init__(parent)
|
|
self.setFixedHeight(40)
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(15, 0, 5, 0)
|
|
|
|
from PyQt6.QtGui import QIcon
|
|
self.icon_label = QLabel()
|
|
icon = QIcon.fromTheme("preferences-desktop-wallpaper", QIcon.fromTheme("image-x-generic"))
|
|
self.icon_label.setPixmap(icon.pixmap(20, 20))
|
|
|
|
self.title = QLabel("Wallpaper Exporter")
|
|
self.title.setStyleSheet("color: #ffffff; font-weight: bold; font-size: 14px;")
|
|
|
|
self.close_btn = QPushButton("✕")
|
|
self.close_btn.setFixedSize(30, 30)
|
|
self.close_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #aaaaaa;
|
|
border: none;
|
|
font-size: 16px;
|
|
border-radius: 4px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #e81123;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.close_btn.clicked.connect(parent.close)
|
|
|
|
layout.addWidget(self.icon_label)
|
|
layout.addSpacing(5)
|
|
layout.addWidget(self.title)
|
|
layout.addStretch()
|
|
layout.addWidget(self.close_btn)
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
window = self.window().windowHandle()
|
|
if window:
|
|
window.startSystemMove()
|
|
event.accept()
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowFlag(Qt.WindowType.FramelessWindowHint)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
self.setFixedSize(450, 350)
|
|
|
|
# Central widget with custom styling for rounded corners
|
|
central_widget = QWidget()
|
|
central_widget.setObjectName("MainWidget")
|
|
central_widget.setStyleSheet("""
|
|
QWidget#MainWidget {
|
|
background-color: #2d2d2d;
|
|
border-radius: 10px;
|
|
border: 1px solid #444444;
|
|
}
|
|
""")
|
|
self.setCentralWidget(central_widget)
|
|
|
|
layout = QVBoxLayout(central_widget)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
self.title_bar = TitleBar(self)
|
|
layout.addWidget(self.title_bar)
|
|
|
|
# Content frame ensures even padding
|
|
content_frame = QWidget()
|
|
content_layout = QVBoxLayout(content_frame)
|
|
content_layout.setContentsMargins(25, 25, 25, 25)
|
|
content_layout.setSpacing(15)
|
|
|
|
self.drop_zone = DropZone()
|
|
self.drop_zone.setFixedWidth(350)
|
|
self.drop_zone.setSizePolicy(
|
|
self.drop_zone.sizePolicy().Policy.Fixed,
|
|
self.drop_zone.sizePolicy().Policy.Expanding
|
|
)
|
|
self.drop_zone.file_dropped.connect(self.start_processing)
|
|
|
|
drop_layout = QHBoxLayout()
|
|
drop_layout.addStretch()
|
|
drop_layout.addWidget(self.drop_zone)
|
|
drop_layout.addStretch()
|
|
content_layout.addLayout(drop_layout)
|
|
|
|
self.status_label = QLabel("Ready")
|
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.status_label.hide()
|
|
content_layout.addWidget(self.status_label)
|
|
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setFixedHeight(12)
|
|
self.progress_bar.hide()
|
|
content_layout.addWidget(self.progress_bar)
|
|
|
|
layout.addWidget(content_frame)
|
|
|
|
self.worker = None
|
|
|
|
def start_processing(self, file_paths: list):
|
|
if len(file_paths) == 1:
|
|
self.status_label.setText(f"Preparing: {file_paths[0].name}")
|
|
else:
|
|
self.status_label.setText(f"Preparing {len(file_paths)} files...")
|
|
|
|
self.status_label.show()
|
|
self.progress_bar.show()
|
|
self.progress_bar.setValue(0)
|
|
self.drop_zone.setDisabled(True)
|
|
|
|
self.worker = ExporterWorker(file_paths)
|
|
self.worker.progress_updated.connect(self.progress_bar.setValue)
|
|
self.worker.status_updated.connect(self.status_label.setText)
|
|
self.worker.finished_success.connect(self.processing_finished)
|
|
self.worker.finished_error.connect(self.processing_failed)
|
|
self.worker.start()
|
|
|
|
def processing_finished(self, out_dir: str):
|
|
self.status_label.hide()
|
|
self.progress_bar.hide()
|
|
self.drop_zone.setDisabled(False)
|
|
QMessageBox.information(self, "Success", f"Images successfully exported to:\n{out_dir}")
|
|
|
|
def processing_failed(self, error_msg: str):
|
|
self.status_label.hide()
|
|
self.progress_bar.hide()
|
|
self.drop_zone.setDisabled(False)
|
|
QMessageBox.critical(self, "Error", f"Failed to export image:\n{error_msg}")
|
|
|
|
|
|
def main():
|
|
app = QApplication(sys.argv)
|
|
app.setStyle("Fusion")
|
|
|
|
# Optional dark mode palette for better look out-of-the-box
|
|
from PyQt6.QtGui import QPalette, QColor
|
|
palette = QPalette()
|
|
palette.setColor(QPalette.ColorRole.Window, QColor(45, 45, 45))
|
|
palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
|
|
app.setPalette(palette)
|
|
|
|
window = MainWindow()
|
|
window.show()
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|