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()