Init projet Photostation - borne impression photo tactile PyQt6

Structure : 4 écrans (accueil, import, éditeur, impression),
navigation QStackedWidget, base éditeur QGraphicsScene.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 18:47:56 +01:00
parent a5b02cb56a
commit 6598e4073f
12 changed files with 539 additions and 0 deletions

0
src/__init__.py Normal file
View File

27
src/main.py Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Photostation - Borne d'impression photo tactile."""
import sys
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt
from ui.main_window import MainWindow
def main():
app = QApplication(sys.argv)
app.setApplicationName("Photostation")
window = MainWindow()
# Mode kiosk si argument --kiosk
if "--kiosk" in sys.argv:
window.showFullScreen()
else:
window.resize(1024, 600)
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

0
src/ui/__init__.py Normal file
View File

47
src/ui/main_window.py Normal file
View File

@@ -0,0 +1,47 @@
"""Fenêtre principale — navigation entre les écrans."""
from PyQt6.QtWidgets import QMainWindow, QStackedWidget
from PyQt6.QtCore import Qt
from ui.screens.home_screen import HomeScreen
from ui.screens.import_screen import ImportScreen
from ui.screens.editor_screen import EditorScreen
from ui.screens.print_screen import PrintScreen
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Photostation")
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True)
self.stack = QStackedWidget()
self.setCentralWidget(self.stack)
# Écrans
self.home_screen = HomeScreen(self)
self.import_screen = ImportScreen(self)
self.editor_screen = EditorScreen(self)
self.print_screen = PrintScreen(self)
self.stack.addWidget(self.home_screen) # 0
self.stack.addWidget(self.import_screen) # 1
self.stack.addWidget(self.editor_screen) # 2
self.stack.addWidget(self.print_screen) # 3
self.show_home()
def show_home(self):
self.stack.setCurrentIndex(0)
def show_import(self):
self.stack.setCurrentIndex(1)
def show_editor(self, image_path=None):
if image_path:
self.editor_screen.load_image(image_path)
self.stack.setCurrentIndex(2)
def show_print(self, image_path=None):
if image_path:
self.print_screen.set_image(image_path)
self.stack.setCurrentIndex(3)

View File

View File

@@ -0,0 +1,144 @@
"""Écran éditeur — recadrage, goodies, effets, multi-pose."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
QToolBar
)
from PyQt6.QtCore import Qt, QRectF
from PyQt6.QtGui import QFont, QPixmap, QImage, QPainter
class EditorScreen(QWidget):
def __init__(self, main_window):
super().__init__()
self.main_window = main_window
self.current_image_path = None
self.pixmap_item = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(10)
# Header
header = QHBoxLayout()
back_btn = QPushButton("Retour")
back_btn.setFont(QFont("", 14))
back_btn.clicked.connect(self.main_window.show_import)
header.addWidget(back_btn)
title = QLabel("Éditeur")
title.setFont(QFont("", 24, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
header.addWidget(title, 1)
print_btn = QPushButton("Imprimer")
print_btn.setFont(QFont("", 14))
print_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 10px;
padding: 10px 20px;
}
QPushButton:pressed { background-color: #388E3C; }
""")
print_btn.clicked.connect(self._go_print)
header.addWidget(print_btn)
layout.addLayout(header)
# Zone d'édition (QGraphicsScene)
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
self.view.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
self.view.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.view.setStyleSheet("background-color: #333;")
layout.addWidget(self.view, 1)
# Barre d'outils
tools = QHBoxLayout()
tools.setSpacing(10)
tool_buttons = [
("Recadrer", self._crop),
("Rotation", self._rotate),
("N&B", self._filter_grayscale),
("Sépia", self._filter_sepia),
("Goodies", self._add_goodies),
("Multi-pose", self._multipose),
("Texte", self._add_text),
]
for label, callback in tool_buttons:
btn = QPushButton(label)
btn.setFont(QFont("", 13))
btn.setMinimumHeight(50)
btn.setStyleSheet("""
QPushButton {
background-color: #555;
color: white;
border-radius: 8px;
padding: 8px 15px;
}
QPushButton:pressed { background-color: #777; }
""")
btn.clicked.connect(callback)
tools.addWidget(btn)
layout.addLayout(tools)
def load_image(self, path):
self.current_image_path = path
self.scene.clear()
pixmap = QPixmap(path)
self.pixmap_item = self.scene.addPixmap(pixmap)
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
def _go_print(self):
# TODO: exporter le canvas édité en image
self.main_window.show_print(self.current_image_path)
def _crop(self):
# TODO: outil de recadrage interactif
pass
def _rotate(self):
if self.pixmap_item:
self.pixmap_item.setRotation(self.pixmap_item.rotation() + 90)
def _filter_grayscale(self):
if not self.current_image_path:
return
pixmap = QPixmap(self.current_image_path)
image = pixmap.toImage().convertToFormat(QImage.Format.Format_Grayscale8)
self.scene.clear()
self.pixmap_item = self.scene.addPixmap(QPixmap.fromImage(image))
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
def _filter_sepia(self):
if not self.current_image_path:
return
from PIL import Image, ImageOps
img = Image.open(self.current_image_path)
sepia = ImageOps.grayscale(img)
sepia = ImageOps.colorize(sepia, "#704214", "#C0A080")
# Convertir PIL -> QPixmap
data = sepia.convert("RGBA").tobytes("raw", "RGBA")
qimg = QImage(data, sepia.width, sepia.height, QImage.Format.Format_RGBA8888)
self.scene.clear()
self.pixmap_item = self.scene.addPixmap(QPixmap.fromImage(qimg))
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
def _add_goodies(self):
# TODO: panneau de sélection de stickers
pass
def _multipose(self):
# TODO: sélection de templates multi-photos
pass
def _add_text(self):
# TODO: ajout de texte éditable sur le canvas
pass

View File

@@ -0,0 +1,77 @@
"""Écran d'accueil — choix du mode d'import."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
class HomeScreen(QWidget):
def __init__(self, main_window):
super().__init__()
self.main_window = main_window
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(40)
# Titre
title = QLabel("Photostation")
title.setFont(QFont("", 48, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
subtitle = QLabel("Sélectionnez un mode d'import")
subtitle.setFont(QFont("", 20))
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(subtitle)
# Boutons d'import
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(20)
modes = [
("USB", "Clé USB", self._import_usb),
("WiFi", "Depuis téléphone", self._import_wifi),
("QR Code", "Scanner pour envoyer", self._import_qr),
("Email", "Recevoir par mail", self._import_email),
]
for label, desc, callback in modes:
btn = self._create_mode_button(label, desc)
btn.clicked.connect(callback)
buttons_layout.addWidget(btn)
layout.addLayout(buttons_layout)
def _create_mode_button(self, label, description):
btn = QPushButton(f"{label}\n{description}")
btn.setFont(QFont("", 16))
btn.setMinimumSize(200, 150)
btn.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
border-radius: 15px;
padding: 20px;
}
QPushButton:pressed {
background-color: #1976D2;
}
""")
return btn
def _import_usb(self):
self.main_window.show_import()
def _import_wifi(self):
self.main_window.show_import()
def _import_qr(self):
self.main_window.show_import()
def _import_email(self):
self.main_window.show_import()

View File

@@ -0,0 +1,87 @@
"""Écran d'import — galerie de photos importées."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QScrollArea, QGridLayout
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QPixmap
class ImportScreen(QWidget):
def __init__(self, main_window):
super().__init__()
self.main_window = main_window
self.photos = []
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Header
header = QHBoxLayout()
back_btn = QPushButton("Retour")
back_btn.setFont(QFont("", 14))
back_btn.clicked.connect(self.main_window.show_home)
header.addWidget(back_btn)
title = QLabel("Sélectionnez une photo")
title.setFont(QFont("", 24, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
header.addWidget(title, 1)
header.addSpacing(80) # Balance le bouton retour
layout.addLayout(header)
# Grille de photos (scrollable)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(10)
scroll.setWidget(self.grid_widget)
layout.addWidget(scroll)
def set_photos(self, photo_paths):
"""Affiche les photos dans la grille."""
# Clear existing
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
self.photos = photo_paths
cols = 4
for i, path in enumerate(photo_paths):
row, col = divmod(i, cols)
thumb = self._create_thumbnail(path)
self.grid_layout.addWidget(thumb, row, col)
def _create_thumbnail(self, path):
btn = QPushButton()
btn.setMinimumSize(180, 180)
btn.setStyleSheet("""
QPushButton {
border: 3px solid transparent;
border-radius: 10px;
}
QPushButton:pressed {
border-color: #2196F3;
}
""")
pixmap = QPixmap(path)
if not pixmap.isNull():
scaled = pixmap.scaled(
170, 170,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
btn.setIcon(scaled)
from PyQt6.QtCore import QSize
btn.setIconSize(QSize(170, 170))
btn.clicked.connect(lambda checked, p=path: self.main_window.show_editor(p))
return btn

View File

@@ -0,0 +1,90 @@
"""Écran d'impression — prévisualisation et lancement impression."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QComboBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QPixmap
class PrintScreen(QWidget):
FORMATS = {
"10x15 cm": (10, 15),
"13x18 cm": (13, 18),
"15x20 cm": (15, 20),
}
def __init__(self, main_window):
super().__init__()
self.main_window = main_window
self.image_path = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(30)
# Header
header = QHBoxLayout()
back_btn = QPushButton("Retour éditeur")
back_btn.setFont(QFont("", 14))
back_btn.clicked.connect(lambda: self.main_window.show_editor())
header.addWidget(back_btn)
header.addStretch()
layout.addLayout(header)
# Prévisualisation
self.preview = QLabel()
self.preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.preview.setMinimumSize(400, 300)
self.preview.setStyleSheet("background-color: #333; border-radius: 10px;")
layout.addWidget(self.preview)
# Sélection format
format_layout = QHBoxLayout()
format_label = QLabel("Format :")
format_label.setFont(QFont("", 16))
format_layout.addWidget(format_label)
self.format_combo = QComboBox()
self.format_combo.setFont(QFont("", 16))
self.format_combo.addItems(self.FORMATS.keys())
self.format_combo.setMinimumHeight(50)
format_layout.addWidget(self.format_combo)
layout.addLayout(format_layout)
# Bouton imprimer
print_btn = QPushButton("IMPRIMER")
print_btn.setFont(QFont("", 24, QFont.Weight.Bold))
print_btn.setMinimumSize(300, 80)
print_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 15px;
}
QPushButton:pressed { background-color: #388E3C; }
""")
print_btn.clicked.connect(self._print)
layout.addWidget(print_btn, alignment=Qt.AlignmentFlag.AlignCenter)
def set_image(self, path):
self.image_path = path
pixmap = QPixmap(path)
if not pixmap.isNull():
scaled = pixmap.scaled(
400, 300,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
self.preview.setPixmap(scaled)
def _print(self):
if not self.image_path:
return
fmt = self.format_combo.currentText()
size = self.FORMATS[fmt]
# TODO: redimensionner selon le format et envoyer à CUPS
print(f"Impression {self.image_path} en {fmt} ({size[0]}x{size[1]} cm)")