diff --git a/app/dialogs/edit_content_dialog.py b/app/dialogs/edit_content_dialog.py new file mode 100644 index 0000000..16527bb --- /dev/null +++ b/app/dialogs/edit_content_dialog.py @@ -0,0 +1,110 @@ +"""Dialogue Edition de contenu — texte et images existants.""" + +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QGroupBox, QListWidget, QListWidgetItem, + QPushButton, QDialogButtonBox, QTextEdit, + QFileDialog, QTabWidget, QWidget, QMessageBox) +from PyQt6.QtCore import Qt + + +class EditContentDialog(QDialog): + def __init__(self, text_items=None, image_items=None, parent=None): + super().__init__(parent) + self.setWindowTitle("Edition de contenu") + self.setMinimumSize(500, 400) + self._text_items = text_items or [] + self._image_items = image_items or [] + self._actions = [] # liste de {"type": "mask_text"|"replace_image", ...} + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + tabs = QTabWidget() + + # Onglet Texte + text_tab = QWidget() + tl = QVBoxLayout(text_tab) + tl.addWidget(QLabel("Texte detecte sur la page (cliquez pour masquer) :")) + self._text_list = QListWidget() + for item in self._text_items: + li = QListWidgetItem(item["text"]) + li.setData(Qt.ItemDataRole.UserRole, item) + self._text_list.addItem(li) + tl.addWidget(self._text_list) + + btn_mask = QPushButton("Masquer le texte selectionne") + btn_mask.clicked.connect(self._mask_text) + tl.addWidget(btn_mask) + + tabs.addTab(text_tab, "Texte") + + # Onglet Images + img_tab = QWidget() + il = QVBoxLayout(img_tab) + il.addWidget(QLabel("Images detectees sur la page :")) + self._img_list = QListWidget() + for item in self._image_items: + li = QListWidgetItem(f"{item['name']} ({item['width']}x{item['height']})") + li.setData(Qt.ItemDataRole.UserRole, item) + self._img_list.addItem(li) + il.addWidget(self._img_list) + + btn_layout = QHBoxLayout() + btn_replace = QPushButton("Remplacer par une image...") + btn_replace.clicked.connect(self._replace_image) + btn_layout.addWidget(btn_replace) + btn_delete = QPushButton("Masquer cette image") + btn_delete.clicked.connect(self._delete_image) + btn_layout.addWidget(btn_delete) + il.addLayout(btn_layout) + + tabs.addTab(img_tab, "Images") + + layout.addWidget(tabs) + + # Actions en attente + self._actions_label = QLabel("Actions en attente : 0") + layout.addWidget(self._actions_label) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def _mask_text(self): + item = self._text_list.currentItem() + if not item: + return + data = item.data(Qt.ItemDataRole.UserRole) + self._actions.append({"type": "mask_text", "text": data["text"], "line_index": data["line_index"]}) + item.setForeground(Qt.GlobalColor.red) + item.setText(f"[MASQUE] {data['text']}") + self._actions_label.setText(f"Actions en attente : {len(self._actions)}") + + def _replace_image(self): + item = self._img_list.currentItem() + if not item: + return + path, _ = QFileDialog.getOpenFileName(self, "Image de remplacement", "", + "Images (*.png *.jpg *.jpeg *.bmp)") + if path: + data = item.data(Qt.ItemDataRole.UserRole) + self._actions.append({"type": "replace_image", "name": data["name"], "new_path": path}) + item.setText(f"[REMPLACE] {data['name']}") + self._actions_label.setText(f"Actions en attente : {len(self._actions)}") + + def _delete_image(self): + item = self._img_list.currentItem() + if not item: + return + data = item.data(Qt.ItemDataRole.UserRole) + self._actions.append({"type": "mask_image", "name": data["name"]}) + item.setForeground(Qt.GlobalColor.red) + item.setText(f"[MASQUE] {data['name']}") + self._actions_label.setText(f"Actions en attente : {len(self._actions)}") + + def get_actions(self): + return self._actions diff --git a/app/engine/extract_content.py b/app/engine/extract_content.py new file mode 100644 index 0000000..fd2230b --- /dev/null +++ b/app/engine/extract_content.py @@ -0,0 +1,90 @@ +"""Extraction de texte et images depuis un PDF.""" + +from pypdf import PdfReader +from .pdf_utils import get_page_size + + +def extract_text_from_page(input_path, page_index): + """Extraire le texte d'une page avec positions approximatives.""" + reader = PdfReader(input_path) + if page_index >= len(reader.pages): + return [] + page = reader.pages[page_index] + text = page.extract_text() or "" + lines = text.split("\n") + w, h = get_page_size(page) + # Positions approximatives (pypdf ne donne pas les coords exactes) + results = [] + line_height = h / max(len(lines), 1) + for i, line in enumerate(lines): + if line.strip(): + results.append({ + "text": line.strip(), + "y_approx": h - (i + 1) * line_height, + "line_index": i, + }) + return results + + +def extract_images_from_page(input_path, page_index): + """Extraire les images d'une page.""" + reader = PdfReader(input_path) + if page_index >= len(reader.pages): + return [] + page = reader.pages[page_index] + images = [] + if "/XObject" in page["/Resources"]: + xobjects = page["/Resources"]["/XObject"].get_object() + for obj_name in xobjects: + obj = xobjects[obj_name].get_object() + if obj["/Subtype"] == "/Image": + width = obj["/Width"] + height = obj["/Height"] + images.append({ + "name": obj_name, + "width": width, + "height": height, + }) + return images + + +def replace_image_in_page(input_path, output_path, page_index, image_name, new_image_path): + """Remplacer une image dans une page (overlay avec masquage).""" + from pypdf import PdfWriter + from reportlab.pdfgen import canvas + from reportlab.lib.utils import ImageReader + import io + + reader = PdfReader(input_path) + writer = PdfWriter() + + for i, page in enumerate(reader.pages): + if i == page_index and new_image_path: + w, h = get_page_size(page) + # On ne peut pas vraiment remplacer l'image dans le flux PDF + # On ajoute la nouvelle image par-dessus (avec un masque blanc si besoin) + packet = io.BytesIO() + c = canvas.Canvas(packet, pagesize=(w, h)) + try: + img = ImageReader(new_image_path) + img_w, img_h = img.getSize() + # Centrer l'image + scale = min(w / img_w, h / img_h) * 0.8 + draw_w = img_w * scale + draw_h = img_h * scale + x = (w - draw_w) / 2 + y = (h - draw_h) / 2 + c.drawImage(new_image_path, x, y, width=draw_w, height=draw_h, + preserveAspectRatio=True, mask='auto') + except Exception: + pass + 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 diff --git a/app/main_window.py b/app/main_window.py index f5ce530..7c3c92b 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -41,7 +41,9 @@ from app.engine.crop import crop_pages from app.engine.add_content import add_text_to_pages, add_image_to_pages from app.engine.unbooklet import unbooklet from app.engine.trim_shift import trim_and_shift +from app.engine.extract_content import extract_text_from_page, extract_images_from_page from app.dialogs.trim_shift_dialog import TrimShiftDialog +from app.dialogs.edit_content_dialog import EditContentDialog class MainWindow(QMainWindow): @@ -236,6 +238,8 @@ class MainWindow(QMainWindow): # Annotations annot_menu = menubar.addMenu("Annotations") + self._add_action(annot_menu, "Editer le contenu...", None, self._do_edit_content) + annot_menu.addSeparator() 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() @@ -331,6 +335,7 @@ class MainWindow(QMainWindow): # Section Annotations rtb.addWidget(self._make_section_label("ANNOTATIONS")) + rtb.addAction("Editer", self._do_edit_content) rtb.addAction("Texte", self._do_add_text) rtb.addAction("Image", self._do_add_image) rtb.addAction("Numeros", self._do_page_numbers) @@ -812,6 +817,72 @@ class MainWindow(QMainWindow): self._hide_progress() QMessageBox.critical(self, "Erreur", str(e)) + def _do_edit_content(self): + if not self._require_file(): + return + cur = self._viewer.current_page + texts = extract_text_from_page(self._current_file, cur) + images = extract_images_from_page(self._current_file, cur) + dlg = EditContentDialog(text_items=texts, image_items=images, parent=self) + if dlg.exec(): + actions = dlg.get_actions() + if not actions: + return + self._show_progress() + from pypdf import PdfReader, PdfWriter + from app.engine.pdf_utils import get_page_size + from reportlab.pdfgen import canvas as rl_canvas + import io + reader = PdfReader(self._current_file) + writer = PdfWriter() + page = reader.pages[cur] + w, h = get_page_size(page) + + # Appliquer les masquages de texte + mask_lines = [a["line_index"] for a in actions if a["type"] == "mask_text"] + if mask_lines: + packet = io.BytesIO() + c = rl_canvas.Canvas(packet, pagesize=(w, h)) + c.setFillColorRGB(1, 1, 1) + line_h = h / max(len(texts), 1) + for li in mask_lines: + y = h - (li + 1) * line_h + c.rect(0, y, w, line_h, fill=1, stroke=0) + c.showPage() + c.save() + packet.seek(0) + overlay = PdfReader(packet) + page.merge_page(overlay.pages[0]) + + # Remplacer les images + for a in actions: + if a["type"] == "replace_image" and a.get("new_path"): + from app.engine.extract_content import replace_image_in_page + # On fait un write intermediaire + tmp = self._make_temp() + for ii, pp in enumerate(reader.pages): + writer.add_page(pp) + with open(tmp, "wb") as f: + writer.write(f) + replace_image_in_page(tmp, tmp, cur, a["name"], a["new_path"]) + reader = PdfReader(tmp) + writer = PdfWriter() + for pp in reader.pages: + writer.add_page(pp) + output = self._make_temp() + with open(output, "wb") as f: + writer.write(f) + self._show_result(output) + return + + for i, p in enumerate(reader.pages): + writer.add_page(p) + output = self._make_temp() + with open(output, "wb") as f: + writer.write(f) + self._show_result(output) + self._status_label.setText("Contenu edite") + def _do_add_text(self): if not self._require_file(): return diff --git a/app/updater.py b/app/updater.py index 1516c94..8a6459b 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 = "3.2.0" +VERSION = "3.3.0" def get_remote_version(): diff --git a/main.py b/main.py index 01ca64a..9a88afb 100644 --- a/main.py +++ b/main.py @@ -207,24 +207,64 @@ def main(): from app.main_window import MainWindow window = MainWindow() - # Ouvrir les fichiers passes en argument - files_to_open = [a for a in sys.argv[1:] if a.lower().endswith('.pdf') and os.path.exists(a)] + # Gestion des arguments + args = sys.argv[1:] + action = None + files_to_open = [] + i = 0 + while i < len(args): + if args[i] == "--action" and i + 1 < len(args): + action = args[i + 1] + i += 2 + elif args[i].lower().endswith('.pdf') and os.path.exists(args[i]): + files_to_open.append(args[i]) + i += 1 + else: + i += 1 + if len(files_to_open) == 1: window._open_pdf(files_to_open[0]) elif len(files_to_open) > 1: - # Plusieurs fichiers = fusionner from pypdf import PdfWriter - import tempfile + import tempfile as tf writer = PdfWriter() for f in files_to_open: writer.append(f) - fd, merged = tempfile.mkstemp(suffix=".pdf") + fd, merged = tf.mkstemp(suffix=".pdf") os.close(fd) with open(merged, "wb") as out: writer.write(out) window._open_pdf(merged) window._status_label.setText(f"{len(files_to_open)} fichiers fusionnes") + # Actions en ligne de commande + if action and files_to_open: + from PyQt6.QtCore import QTimer + def run_cli_action(): + from app.engine.booklet import create_booklet + from app.engine.nup import nup_pages + from app.engine.unbooklet import unbooklet + import tempfile as tf + fd, output = tf.mkstemp(suffix=".pdf") + os.close(fd) + src = files_to_open[0] + try: + if action == "booklet_a4": + create_booklet(src, output, binding="saddle_stitch", sheet_size=(841.89, 595.28)) + elif action == "booklet_a3": + create_booklet(src, output, binding="saddle_stitch", sheet_size=(1190.55, 841.89)) + elif action == "nup_2x2": + nup_pages(src, output, cols=2, rows=2, sheet_size=(841.89, 595.28)) + elif action == "unbooklet": + unbooklet(src, output) + else: + return + window._show_result(output) + window._status_label.setText(f"Action '{action}' executee") + except Exception as e: + QMessageBox.critical(window, "Erreur", str(e)) + QTimer.singleShot(500, run_cli_action) + window.show() splash.close() sys.exit(app.exec()) diff --git a/register_windows.bat b/register_windows.bat index 1931b10..bbefc2f 100644 --- a/register_windows.bat +++ b/register_windows.bat @@ -74,6 +74,12 @@ reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\s reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3" /ve /d "Livret A3" /f >nul 2>&1 reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3\command" /ve /d "\"%EXE_PATH%\" --action booklet_a3 \"%%1\"" /f >nul 2>&1 +reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\nup2x2" /ve /d "2x2 par feuille" /f >nul 2>&1 +reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\nup2x2\command" /ve /d "\"%EXE_PATH%\" --action nup_2x2 \"%%1\"" /f >nul 2>&1 + +reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\unbooklet" /ve /d "Delivretiser" /f >nul 2>&1 +reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\unbooklet\command" /ve /d "\"%EXE_PATH%\" --action unbooklet \"%%1\"" /f >nul 2>&1 + echo [OK] Menu contextuel ajoute echo (clic droit sur PDF > CopyDev Impose > ...) goto :EOF diff --git a/version.txt b/version.txt index 944880f..15a2799 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.2.0 +3.3.0