V2.3.0 — Ajout texte/image + barre outils verticale

- Ajout de texte libre sur les pages (Annotations > Ajouter du texte)
  - Texte multi-ligne, police, taille, couleur au choix
  - Position X/Y en mm, appliquer a page courante/toutes/paires/impaires
- Ajout d'image sur les pages (Annotations > Ajouter une image)
  - PNG/JPG/BMP/GIF/TIFF, position, taille, ratio auto
  - Memes options d'application
- Barre d'outils verticale a droite avec toutes les fonctions :
  - IMPOSITION : Livret, N-Up, Repetition, Manuel, Joindre
  - PAGES : Recadrer, Pg blanche, Fusionner
  - ANNOTATIONS : Texte, Image, Numeros, Masquer
  - REPERES : Coupe +/-, Bleed +/-
- Toolbar horizontale : Ouvrir, Enregistrer, Imprimer, Zoom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-21 13:50:55 +01:00
parent 500309ed53
commit e7a27018e5
6 changed files with 427 additions and 5 deletions

View File

@@ -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,
}

View File

@@ -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,
}

86
app/engine/add_content.py Normal file
View File

@@ -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

View File

@@ -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

View File

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