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:
120
app/dialogs/add_image_dialog.py
Normal file
120
app/dialogs/add_image_dialog.py
Normal 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,
|
||||
}
|
||||
117
app/dialogs/add_text_dialog.py
Normal file
117
app/dialogs/add_text_dialog.py
Normal 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
86
app/engine/add_content.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user