diff --git a/app/dialogs/add_image_dialog.py b/app/dialogs/add_image_dialog.py new file mode 100644 index 0000000..3a55aaa --- /dev/null +++ b/app/dialogs/add_image_dialog.py @@ -0,0 +1,120 @@ +"""Dialogue Ajouter une image sur une page.""" + +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QGroupBox, QComboBox, QDoubleSpinBox, + QDialogButtonBox, QPushButton, QFileDialog) +from reportlab.lib.units import mm + + +class AddImageDialog(QDialog): + def __init__(self, total_pages=1, current_page=0, parent=None): + super().__init__(parent) + self.setWindowTitle("Ajouter une image") + self.setMinimumWidth(400) + self._total_pages = total_pages + self._current_page = current_page + self._image_path = None + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + # Image + grp_img = QGroupBox("Image") + il = QHBoxLayout(grp_img) + self.btn_choose = QPushButton("Choisir une image...") + self.btn_choose.clicked.connect(self._choose_image) + il.addWidget(self.btn_choose) + self.lbl_file = QLabel("Aucun fichier") + self.lbl_file.setStyleSheet("color: #888;") + il.addWidget(self.lbl_file) + layout.addWidget(grp_img) + + # Position et taille + grp_pos = QGroupBox("Position et taille (mm)") + pl = QVBoxLayout(grp_pos) + + row1 = QHBoxLayout() + row1.addWidget(QLabel("X :")) + self.spin_x = QDoubleSpinBox() + self.spin_x.setRange(0, 2000) + self.spin_x.setValue(20) + self.spin_x.setSuffix(" mm") + row1.addWidget(self.spin_x) + row1.addWidget(QLabel("Y :")) + self.spin_y = QDoubleSpinBox() + self.spin_y.setRange(0, 2000) + self.spin_y.setValue(20) + self.spin_y.setSuffix(" mm") + row1.addWidget(self.spin_y) + pl.addLayout(row1) + + row2 = QHBoxLayout() + row2.addWidget(QLabel("Largeur :")) + self.spin_w = QDoubleSpinBox() + self.spin_w.setRange(1, 2000) + self.spin_w.setValue(50) + self.spin_w.setSuffix(" mm") + row2.addWidget(self.spin_w) + row2.addWidget(QLabel("Hauteur :")) + self.spin_h = QDoubleSpinBox() + self.spin_h.setRange(0, 2000) + self.spin_h.setValue(0) + self.spin_h.setSuffix(" mm") + self.spin_h.setSpecialValueText("Auto") + row2.addWidget(self.spin_h) + pl.addLayout(row2) + + pl.addWidget(QLabel("Hauteur 0 = proportionnelle a la largeur")) + layout.addWidget(grp_pos) + + # Appliquer a + grp_apply = QGroupBox("Appliquer a") + al = QHBoxLayout(grp_apply) + self.combo_apply = QComboBox() + self.combo_apply.addItems([ + f"Page courante ({self._current_page + 1})", + "Toutes les pages", + "Pages paires", + "Pages impaires" + ]) + al.addWidget(self.combo_apply) + layout.addWidget(grp_apply) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def _choose_image(self): + path, _ = QFileDialog.getOpenFileName( + self, "Choisir une image", "", + "Images (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)" + ) + if path: + self._image_path = path + self.lbl_file.setText(path.split("/")[-1].split("\\")[-1]) + self.lbl_file.setStyleSheet("color: #0066cc;") + + def get_settings(self): + apply_text = self.combo_apply.currentText() + if "courante" in apply_text: + apply_to = "current" + elif "paires" in apply_text.lower(): + apply_to = "even" + elif "impaires" in apply_text.lower(): + apply_to = "odd" + else: + apply_to = "all" + + return { + "image_path": self._image_path, + "x": self.spin_x.value() * mm, + "y": self.spin_y.value() * mm, + "width": self.spin_w.value() * mm, + "height": self.spin_h.value() * mm if self.spin_h.value() > 0 else None, + "apply_to": apply_to, + "current_page": self._current_page, + } diff --git a/app/dialogs/add_text_dialog.py b/app/dialogs/add_text_dialog.py new file mode 100644 index 0000000..4de7b4c --- /dev/null +++ b/app/dialogs/add_text_dialog.py @@ -0,0 +1,117 @@ +"""Dialogue Ajouter du texte sur une page.""" + +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QGroupBox, QComboBox, QSpinBox, QLineEdit, + QDialogButtonBox, QDoubleSpinBox, QTextEdit, + QColorDialog, QPushButton) +from PyQt6.QtGui import QColor +from PyQt6.QtCore import Qt +from reportlab.lib.units import mm + + +class AddTextDialog(QDialog): + def __init__(self, total_pages=1, current_page=0, parent=None): + super().__init__(parent) + self.setWindowTitle("Ajouter du texte") + self.setMinimumWidth(450) + self._total_pages = total_pages + self._current_page = current_page + self._color = QColor(0, 0, 0) + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + # Texte + grp_text = QGroupBox("Texte") + tl = QVBoxLayout(grp_text) + self.edit_text = QTextEdit() + self.edit_text.setPlaceholderText("Saisissez le texte a ajouter...") + self.edit_text.setMaximumHeight(80) + tl.addWidget(self.edit_text) + layout.addWidget(grp_text) + + # Police + grp_font = QGroupBox("Police") + fl = QHBoxLayout(grp_font) + self.combo_font = QComboBox() + self.combo_font.addItems(["Helvetica", "Helvetica-Bold", "Times-Roman", + "Times-Bold", "Courier", "Courier-Bold"]) + fl.addWidget(self.combo_font) + fl.addWidget(QLabel("Taille :")) + self.spin_size = QSpinBox() + self.spin_size.setRange(4, 200) + self.spin_size.setValue(12) + fl.addWidget(self.spin_size) + self.btn_color = QPushButton("Couleur") + self.btn_color.setStyleSheet("background: black; color: white; padding: 4px 12px;") + self.btn_color.clicked.connect(self._choose_color) + fl.addWidget(self.btn_color) + layout.addWidget(grp_font) + + # Position + grp_pos = QGroupBox("Position (mm depuis le bas-gauche)") + pl = QHBoxLayout(grp_pos) + pl.addWidget(QLabel("X :")) + self.spin_x = QDoubleSpinBox() + self.spin_x.setRange(0, 2000) + self.spin_x.setValue(20) + self.spin_x.setSuffix(" mm") + pl.addWidget(self.spin_x) + pl.addWidget(QLabel("Y :")) + self.spin_y = QDoubleSpinBox() + self.spin_y.setRange(0, 2000) + self.spin_y.setValue(20) + self.spin_y.setSuffix(" mm") + pl.addWidget(self.spin_y) + layout.addWidget(grp_pos) + + # Appliquer a + grp_apply = QGroupBox("Appliquer a") + al = QHBoxLayout(grp_apply) + self.combo_apply = QComboBox() + self.combo_apply.addItems([ + f"Page courante ({self._current_page + 1})", + "Toutes les pages", + "Pages paires", + "Pages impaires" + ]) + al.addWidget(self.combo_apply) + layout.addWidget(grp_apply) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def _choose_color(self): + color = QColorDialog.getColor(self._color, self) + if color.isValid(): + self._color = color + self.btn_color.setStyleSheet( + f"background: {color.name()}; color: {'white' if color.lightness() < 128 else 'black'}; padding: 4px 12px;" + ) + + def get_settings(self): + apply_text = self.combo_apply.currentText() + if "courante" in apply_text: + apply_to = "current" + elif "paires" in apply_text.lower(): + apply_to = "even" + elif "impaires" in apply_text.lower(): + apply_to = "odd" + else: + apply_to = "all" + + return { + "text": self.edit_text.toPlainText(), + "font_name": self.combo_font.currentText(), + "font_size": self.spin_size.value(), + "color": (self._color.redF(), self._color.greenF(), self._color.blueF()), + "x": self.spin_x.value() * mm, + "y": self.spin_y.value() * mm, + "apply_to": apply_to, + "current_page": self._current_page, + } diff --git a/app/engine/add_content.py b/app/engine/add_content.py new file mode 100644 index 0000000..ee42df2 --- /dev/null +++ b/app/engine/add_content.py @@ -0,0 +1,86 @@ +"""Moteur ajout de contenu (texte, image) sur les pages.""" + +from pypdf import PdfReader, PdfWriter +from reportlab.pdfgen import canvas +from reportlab.lib.utils import ImageReader +from .pdf_utils import get_page_size +import io + + +def add_text_to_pages(input_path, output_path, text, font_name="Helvetica", + font_size=12, color=(0, 0, 0), x=0, y=0, + apply_to="all", current_page=0): + """Ajouter du texte sur les pages.""" + reader = PdfReader(input_path) + writer = PdfWriter() + + for i, page in enumerate(reader.pages): + if _should_apply(i, apply_to, current_page): + w, h = get_page_size(page) + packet = io.BytesIO() + c = canvas.Canvas(packet, pagesize=(w, h)) + c.setFont(font_name, font_size) + c.setFillColorRGB(*color) + + # Multi-ligne + lines = text.split("\n") + line_height = font_size * 1.3 + for j, line in enumerate(lines): + c.drawString(x, y - j * line_height, line) + + c.showPage() + c.save() + packet.seek(0) + overlay = PdfReader(packet) + page.merge_page(overlay.pages[0]) + + writer.add_page(page) + + with open(output_path, "wb") as f: + writer.write(f) + return output_path + + +def add_image_to_pages(input_path, output_path, image_path, x=0, y=0, + width=100, height=None, apply_to="all", current_page=0): + """Ajouter une image sur les pages.""" + reader = PdfReader(input_path) + writer = PdfWriter() + + img = ImageReader(image_path) + img_w, img_h = img.getSize() + + if height is None or height <= 0: + height = width * (img_h / img_w) + + for i, page in enumerate(reader.pages): + if _should_apply(i, apply_to, current_page): + w, h = get_page_size(page) + packet = io.BytesIO() + c = canvas.Canvas(packet, pagesize=(w, h)) + c.drawImage(image_path, x, y, width=width, height=height, + preserveAspectRatio=True, mask='auto') + c.showPage() + c.save() + packet.seek(0) + overlay = PdfReader(packet) + page.merge_page(overlay.pages[0]) + + writer.add_page(page) + + with open(output_path, "wb") as f: + writer.write(f) + return output_path + + +def _should_apply(page_index, apply_to, current_page): + page_num = page_index + 1 + if apply_to == "all": + return True + elif apply_to == "current": + return page_index == current_page + elif apply_to == "even": + return page_num % 2 == 0 + elif apply_to == "odd": + return page_num % 2 != 0 + return False diff --git a/app/main_window.py b/app/main_window.py index 2cb7376..b9ab56a 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -24,6 +24,8 @@ from app.dialogs.masking_dialog import MaskingDialog from app.dialogs.bleeds_dialog import BleedsDialog from app.dialogs.crop_marks_dialog import CropMarksDialog from app.dialogs.crop_dialog import CropDialog +from app.dialogs.add_text_dialog import AddTextDialog +from app.dialogs.add_image_dialog import AddImageDialog from app.engine.booklet import create_booklet from app.engine.nup import nup_pages @@ -36,6 +38,7 @@ from app.engine.masking import apply_masking from app.engine.bleeds import define_bleeds from app.engine.crop_marks import add_crop_marks, remove_crop_marks from app.engine.crop import crop_pages +from app.engine.add_content import add_text_to_pages, add_image_to_pages class MainWindow(QMainWindow): @@ -124,6 +127,9 @@ class MainWindow(QMainWindow): # Annotations annot_menu = menubar.addMenu("Annotations") + self._add_action(annot_menu, "Ajouter du texte...", None, self._do_add_text) + self._add_action(annot_menu, "Ajouter une image...", None, self._do_add_image) + annot_menu.addSeparator() self._add_action(annot_menu, "Numerotation de pages", None, self._do_page_numbers) self._add_action(annot_menu, "Masquage (ruban blanc)", None, self._do_masking) annot_menu.addSeparator() @@ -145,15 +151,16 @@ class MainWindow(QMainWindow): act.triggered.connect(callback) menu.addAction(act) - # ── Toolbar ── + # ── Toolbars ── def _setup_toolbar(self): - tb = QToolBar() + # Toolbar horizontale en haut (fichier + zoom) + tb = QToolBar("Fichier") tb.setMovable(False) self.addToolBar(tb) - tb.addAction("Ouvrir", self._open_file) tb.addAction("Enregistrer", self._save_result) + tb.addAction("Imprimer", self._do_print) tb.addSeparator() tb.addAction("Zoom +", lambda: self._viewer.zoom_in()) tb.addAction("Zoom -", lambda: self._viewer.zoom_out()) @@ -163,6 +170,57 @@ class MainWindow(QMainWindow): self._page_label.setStyleSheet("color: #aaa; padding: 0 10px;") tb.addWidget(self._page_label) + # Toolbar verticale a droite (outils) + rtb = QToolBar("Outils") + rtb.setMovable(False) + rtb.setOrientation(Qt.Orientation.Vertical) + rtb.setStyleSheet(""" + QToolBar { background: #12122a; border-left: 1px solid #333; padding: 4px 2px; spacing: 2px; } + QToolBar QToolButton { + color: #ccc; padding: 6px; border-radius: 4px; + font-size: 10px; min-width: 60px; + } + QToolBar QToolButton:hover { background: #0f3460; color: white; } + """) + self.addToolBar(Qt.ToolBarArea.RightToolBarArea, rtb) + + # Section Imposition + rtb.addWidget(self._make_section_label("IMPOSITION")) + rtb.addAction("Livret", self._do_booklet) + rtb.addAction("N-Up", self._do_nup) + rtb.addAction("Repetition", self._do_step_repeat) + rtb.addAction("Manuel", self._do_manual_impose) + rtb.addAction("Joindre", self._do_join_pages) + rtb.addSeparator() + + # Section Pages + rtb.addWidget(self._make_section_label("PAGES")) + rtb.addAction("Recadrer", self._do_crop) + rtb.addAction("Pg blanche", self._do_insert_blank) + rtb.addAction("Fusionner", self._do_merge) + rtb.addSeparator() + + # Section Annotations + rtb.addWidget(self._make_section_label("ANNOTATIONS")) + rtb.addAction("Texte", self._do_add_text) + rtb.addAction("Image", self._do_add_image) + rtb.addAction("Numeros", self._do_page_numbers) + rtb.addAction("Masquer", self._do_masking) + rtb.addSeparator() + + # Section Reperes + rtb.addWidget(self._make_section_label("REPERES")) + rtb.addAction("Coupe +", self._do_crop_marks) + rtb.addAction("Coupe -", self._do_remove_crop_marks) + rtb.addAction("Bleed +", self._do_bleeds) + rtb.addAction("Bleed -", self._do_remove_bleeds) + + def _make_section_label(self, text): + lbl = QLabel(text) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setStyleSheet("color: #555; font-size: 9px; font-weight: bold; letter-spacing: 1px; padding: 4px 0 2px 0;") + return lbl + # ── UI principale ── def _setup_ui(self): @@ -499,6 +557,47 @@ class MainWindow(QMainWindow): self._hide_progress() QMessageBox.critical(self, "Erreur", str(e)) + def _do_add_text(self): + if not self._require_file(): + return + from pypdf import PdfReader + reader = PdfReader(self._current_file) + dlg = AddTextDialog(total_pages=len(reader.pages), + current_page=self._viewer.current_page, parent=self) + if dlg.exec(): + settings = dlg.get_settings() + if not settings["text"].strip(): + return + self._show_progress() + output = self._make_temp() + try: + add_text_to_pages(self._current_file, output, **settings) + self._show_result(output) + except Exception as e: + self._hide_progress() + QMessageBox.critical(self, "Erreur", str(e)) + + def _do_add_image(self): + if not self._require_file(): + return + from pypdf import PdfReader + reader = PdfReader(self._current_file) + dlg = AddImageDialog(total_pages=len(reader.pages), + current_page=self._viewer.current_page, parent=self) + if dlg.exec(): + settings = dlg.get_settings() + if not settings.get("image_path"): + QMessageBox.warning(self, "Erreur", "Aucune image selectionnee.") + return + self._show_progress() + output = self._make_temp() + try: + add_image_to_pages(self._current_file, output, **settings) + self._show_result(output) + except Exception as e: + self._hide_progress() + QMessageBox.critical(self, "Erreur", str(e)) + def _do_crop(self): if not self._require_file(): return diff --git a/app/updater.py b/app/updater.py index 42305c1..eb29f38 100644 --- a/app/updater.py +++ b/app/updater.py @@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool" BRANCH = "master" # Version actuelle (incrementee a chaque release) -VERSION = "2.2.0" +VERSION = "2.3.0" def get_remote_version(): diff --git a/version.txt b/version.txt index ccbccc3..276cbf9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.2.0 +2.3.0