V3.6.0 — Demonter pages, masquage visuel, regles, bleeds, corrections

- Demonter les pages : couper en 2 sans reordonner (sous Delivretiser)
- Masquage visuel : selection au curseur sur l'apercu + plage de pages
- Regles horizontale/verticale en mm (menu Affichage)
- Fonds perdus : filigrane rouge pour visualiser les bleeds
- Reperes de coupe : marques a l'exterieur (page agrandie), 2 traits/coin
- Numerotation : couleur configurable via selecteur
- Decalage : Trim & Shift renomme en francais, creep -> chasse
- Recadrages multiples cumulatifs (plages differentes)
- Fix spinbox format personnalise (fleches aleatoires)
- Fix combo livret : option Automatique preservee

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-29 22:12:54 +02:00
parent f7ad806b26
commit 202834a794
13 changed files with 675 additions and 113 deletions

View File

@@ -64,12 +64,18 @@ class CustomSizeDialog(QDialog):
self.spin_w.setRange(10, 5000) self.spin_w.setRange(10, 5000)
self.spin_w.setValue(210) self.spin_w.setValue(210)
self.spin_w.setSuffix(" mm") self.spin_w.setSuffix(" mm")
self.spin_w.setDecimals(1)
self.spin_w.setSingleStep(1.0)
self.spin_w.setMinimumWidth(100)
gl.addWidget(self.spin_w) gl.addWidget(self.spin_w)
gl.addWidget(QLabel("Hauteur :")) gl.addWidget(QLabel("Hauteur :"))
self.spin_h = QDoubleSpinBox() self.spin_h = QDoubleSpinBox()
self.spin_h.setRange(10, 5000) self.spin_h.setRange(10, 5000)
self.spin_h.setValue(297) self.spin_h.setValue(297)
self.spin_h.setSuffix(" mm") self.spin_h.setSuffix(" mm")
self.spin_h.setDecimals(1)
self.spin_h.setSingleStep(1.0)
self.spin_h.setMinimumWidth(100)
gl.addWidget(self.spin_h) gl.addWidget(self.spin_h)
layout.addWidget(grp) layout.addWidget(grp)

View File

@@ -12,10 +12,27 @@ ADD_LABEL = "Ajouter un format..."
def populate_format_combo(combo, default="A4 Paysage"): def populate_format_combo(combo, default="A4 Paysage"):
"""Remplir un combo avec les formats standard + custom + options.""" """Remplir un combo avec les formats standard + custom + options.
Si default n'est pas un format standard, il est conserve en tete du combo.
"""
combo.blockSignals(True) combo.blockSignals(True)
# Sauvegarder l'item par defaut s'il n'est pas un format standard
keep_first = None
if default and default not in PAGE_SIZES:
# Verifier s'il est deja dans le combo avec des data
idx = combo.findText(default)
if idx >= 0:
keep_first = (default, combo.itemData(idx))
combo.clear() combo.clear()
# Re-ajouter l'item special en tete si necessaire
if keep_first:
combo.addItem(keep_first[0], keep_first[1])
combo.insertSeparator(combo.count())
# Formats standard # Formats standard
for name in PAGE_SIZES: for name in PAGE_SIZES:
combo.addItem(name, PAGE_SIZES[name]) combo.addItem(name, PAGE_SIZES[name])

View File

@@ -1,61 +1,109 @@
"""Dialogue Masquage (rectangles blancs).""" """Dialogue Masquage (rectangles blancs) — avec selection visuelle."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QDoubleSpinBox, QGroupBox, QComboBox, QDoubleSpinBox,
QDialogButtonBox, QPushButton, QListWidget, QDialogButtonBox, QPushButton, QListWidget,
QListWidgetItem) QLineEdit, QRadioButton)
from app.engine.pdf_utils import mm_to_pt from PyQt6.QtCore import Qt
from app.engine.pdf_utils import mm_to_pt, pt_to_mm
class MaskingDialog(QDialog): class MaskingDialog(QDialog):
def __init__(self, parent=None): def __init__(self, page_width_pt=595, page_height_pt=842, total_pages=1,
current_page=0, viewer=None, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Masquage (ruban blanc)") self.setWindowTitle("Masquage (ruban blanc)")
self.setMinimumWidth(450) self.setFixedWidth(380)
self.setModal(False)
self.setWindowFlags(self.windowFlags() | Qt.WindowType.Tool)
self._page_w = page_width_pt
self._page_h = page_height_pt
self._total_pages = total_pages
self._current_page = current_page
self._viewer = viewer
self._rectangles = [] self._rectangles = []
self._setup_ui() self._setup_ui()
def _setup_ui(self): def _setup_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Definition rectangle # Info
grp_rect = QGroupBox("Definir un rectangle de masquage (mm)") pw_mm = pt_to_mm(self._page_w)
rl = QHBoxLayout(grp_rect) ph_mm = pt_to_mm(self._page_h)
info = QLabel(f"Page : {pw_mm:.1f} x {ph_mm:.1f} mm")
info.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(info)
rl.addWidget(QLabel("X:")) # Bouton selection visuelle
if self._viewer:
sel_layout = QHBoxLayout()
self.btn_select = QPushButton("Selectionner la zone sur l'apercu")
self.btn_select.setCheckable(True)
self.btn_select.setStyleSheet("""
QPushButton { background: #00b4d8; color: white; padding: 6px 14px;
border-radius: 6px; font-weight: bold; font-size: 11px; }
QPushButton:checked { background: #e74c3c; }
QPushButton:hover { background: #0096b7; }
QPushButton:checked:hover { background: #c0392b; }
""")
self.btn_select.toggled.connect(self._toggle_select_mode)
sel_layout.addWidget(self.btn_select)
sel_layout.addStretch()
layout.addLayout(sel_layout)
self._viewer.selection_changed.connect(self._on_selection_changed)
# Coordonnees manuelles
grp_rect = QGroupBox("Zone de masquage (mm)")
rl = QVBoxLayout(grp_rect)
row1 = QHBoxLayout()
row1.addWidget(QLabel("X :"))
self.spin_x = QDoubleSpinBox() self.spin_x = QDoubleSpinBox()
self.spin_x.setRange(0, 1000) self.spin_x.setRange(0, 2000)
self.spin_x.setSuffix(" mm") self.spin_x.setSuffix(" mm")
rl.addWidget(self.spin_x) row1.addWidget(self.spin_x)
row1.addWidget(QLabel("Y :"))
rl.addWidget(QLabel("Y:"))
self.spin_y = QDoubleSpinBox() self.spin_y = QDoubleSpinBox()
self.spin_y.setRange(0, 1000) self.spin_y.setRange(0, 2000)
self.spin_y.setSuffix(" mm") self.spin_y.setSuffix(" mm")
rl.addWidget(self.spin_y) row1.addWidget(self.spin_y)
rl.addLayout(row1)
rl.addWidget(QLabel("L:")) row2 = QHBoxLayout()
row2.addWidget(QLabel("Largeur :"))
self.spin_w = QDoubleSpinBox() self.spin_w = QDoubleSpinBox()
self.spin_w.setRange(1, 1000) self.spin_w.setRange(1, 2000)
self.spin_w.setValue(50) self.spin_w.setValue(50)
self.spin_w.setSuffix(" mm") self.spin_w.setSuffix(" mm")
rl.addWidget(self.spin_w) row2.addWidget(self.spin_w)
row2.addWidget(QLabel("Hauteur :"))
rl.addWidget(QLabel("H:"))
self.spin_h = QDoubleSpinBox() self.spin_h = QDoubleSpinBox()
self.spin_h.setRange(1, 1000) self.spin_h.setRange(1, 2000)
self.spin_h.setValue(10) self.spin_h.setValue(10)
self.spin_h.setSuffix(" mm") self.spin_h.setSuffix(" mm")
rl.addWidget(self.spin_h) row2.addWidget(self.spin_h)
rl.addLayout(row2)
layout.addWidget(grp_rect) layout.addWidget(grp_rect)
# Boutons ajouter/supprimer # Bouton ajouter
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
btn_add = QPushButton("Ajouter") self.btn_add = QPushButton("Ajouter ce masque")
btn_add.clicked.connect(self._add_rect) self.btn_add.setStyleSheet("""
btn_layout.addWidget(btn_add) QPushButton { background: #2ecc71; color: white; padding: 6px 14px;
border-radius: 6px; font-weight: bold; }
QPushButton:hover { background: #27ae60; }
""")
self.btn_add.clicked.connect(self._add_rect)
btn_layout.addWidget(self.btn_add)
btn_del = QPushButton("Supprimer") btn_del = QPushButton("Supprimer")
btn_del.setStyleSheet("""
QPushButton { background: #e8e8e8; color: #555; padding: 6px 14px;
border-radius: 6px; border: 1px solid #ccc; }
QPushButton:hover { background: #ddd; }
""")
btn_del.clicked.connect(self._del_rect) btn_del.clicked.connect(self._del_rect)
btn_layout.addWidget(btn_del) btn_layout.addWidget(btn_del)
btn_layout.addStretch() btn_layout.addStretch()
@@ -63,15 +111,31 @@ class MaskingDialog(QDialog):
# Liste des rectangles # Liste des rectangles
self.list_rects = QListWidget() self.list_rects = QListWidget()
self.list_rects.setMaximumHeight(100)
layout.addWidget(self.list_rects) layout.addWidget(self.list_rects)
# Appliquer a # Appliquer a
grp_apply = QGroupBox("Appliquer a") grp_apply = QGroupBox("Appliquer a")
al = QHBoxLayout(grp_apply) al = QVBoxLayout(grp_apply)
self.combo_apply = QComboBox() self.rb_current = QRadioButton(f"Page courante ({self._current_page + 1})")
self.combo_apply.addItems(["Toutes les pages", "Pages paires", "Pages impaires"]) self.rb_current.setChecked(True)
al.addWidget(self.combo_apply) self.rb_all = QRadioButton("Toutes les pages")
self.rb_even = QRadioButton("Pages paires")
self.rb_odd = QRadioButton("Pages impaires")
self.rb_range = QRadioButton("Plage de pages :")
al.addWidget(self.rb_current)
al.addWidget(self.rb_all)
al.addWidget(self.rb_even)
al.addWidget(self.rb_odd)
range_layout = QHBoxLayout()
range_layout.addWidget(self.rb_range)
self.edit_range = QLineEdit()
self.edit_range.setPlaceholderText("ex: 1-3, 5, 8-10")
self.edit_range.setEnabled(False)
range_layout.addWidget(self.edit_range)
al.addLayout(range_layout)
layout.addWidget(grp_apply) layout.addWidget(grp_apply)
self.rb_range.toggled.connect(self.edit_range.setEnabled)
# Boutons # Boutons
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
@@ -81,32 +145,128 @@ class MaskingDialog(QDialog):
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
layout.addWidget(buttons) layout.addWidget(buttons)
def _toggle_select_mode(self, checked):
if self._viewer:
self._viewer.set_select_mode(checked)
if checked:
self.btn_select.setText("Mode selection actif — dessiner la zone")
else:
self.btn_select.setText("Selectionner la zone sur l'apercu")
def _on_selection_changed(self, left, top, right, bottom):
"""Recoit les ratios de selection du viewer."""
pw_mm = pt_to_mm(self._page_w)
ph_mm = pt_to_mm(self._page_h)
x_mm = left * pw_mm
y_mm = top * ph_mm
w_mm = (right - left) * pw_mm
h_mm = (bottom - top) * ph_mm
if w_mm > 0 and h_mm > 0:
self.spin_x.blockSignals(True)
self.spin_y.blockSignals(True)
self.spin_w.blockSignals(True)
self.spin_h.blockSignals(True)
self.spin_x.setValue(x_mm)
self.spin_y.setValue(y_mm)
self.spin_w.setValue(w_mm)
self.spin_h.setValue(h_mm)
self.spin_x.blockSignals(False)
self.spin_y.blockSignals(False)
self.spin_w.blockSignals(False)
self.spin_h.blockSignals(False)
def _add_rect(self): def _add_rect(self):
x_mm = self.spin_x.value()
y_mm = self.spin_y.value()
w_mm = self.spin_w.value()
h_mm = self.spin_h.value()
# Convertir Y : dans le viewer le Y part du haut, dans le PDF du bas
page_h_mm = pt_to_mm(self._page_h)
pdf_y_mm = page_h_mm - y_mm - h_mm
rect = { rect = {
"x": mm_to_pt(self.spin_x.value()), "x": mm_to_pt(x_mm),
"y": mm_to_pt(self.spin_y.value()), "y": mm_to_pt(pdf_y_mm),
"width": mm_to_pt(self.spin_w.value()), "width": mm_to_pt(w_mm),
"height": mm_to_pt(self.spin_h.value()), "height": mm_to_pt(h_mm),
} }
self._rectangles.append(rect) self._rectangles.append(rect)
self.list_rects.addItem( self.list_rects.addItem(
f"X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} " f"X={x_mm:.1f} Y={y_mm:.1f} L={w_mm:.1f} H={h_mm:.1f} mm"
f"L={self.spin_w.value():.1f} H={self.spin_h.value():.1f} mm"
) )
# Nettoyer la selection pour en dessiner une autre
if self._viewer:
self._viewer.clear_selection()
def _del_rect(self): def _del_rect(self):
row = self.list_rects.currentRow() row = self.list_rects.currentRow()
if row >= 0: if row >= 0:
self.list_rects.takeItem(row) self.list_rects.takeItem(row)
self._rectangles.pop(row) self._rectangles.pop(row)
def _parse_range(self):
text = self.edit_range.text().strip()
if not text:
return []
indices = []
for part in text.split(","):
part = part.strip()
if "-" in part:
try:
start, end = part.split("-", 1)
for p in range(int(start), int(end) + 1):
if 1 <= p <= self._total_pages:
indices.append(p - 1)
except ValueError:
pass
else:
try:
p = int(part)
if 1 <= p <= self._total_pages:
indices.append(p - 1)
except ValueError:
pass
return sorted(set(indices))
def closeEvent(self, event):
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
super().closeEvent(event)
def reject(self):
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
super().reject()
def get_settings(self): def get_settings(self):
apply_map = { if self._viewer:
"Toutes les pages": "all", self._viewer.set_select_mode(False)
"Pages paires": "even", self._viewer.clear_selection()
"Pages impaires": "odd"
} # Determiner apply_to
if self.rb_current.isChecked():
apply_to = self._current_page + 1 # 1-based pour le moteur
elif self.rb_all.isChecked():
apply_to = "all"
elif self.rb_even.isChecked():
apply_to = "even"
elif self.rb_odd.isChecked():
apply_to = "odd"
else:
# Plage personnalisee
pages = self._parse_range()
if pages:
apply_to = pages # liste d'indices 0-based
else:
apply_to = "all"
return { return {
"rectangles": self._rectangles, "rectangles": self._rectangles,
"apply_to": apply_map[self.combo_apply.currentText()], "apply_to": apply_to,
} }

View File

@@ -2,7 +2,9 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QSpinBox, QLineEdit, QGroupBox, QComboBox, QSpinBox, QLineEdit,
QDialogButtonBox, QDoubleSpinBox) QDialogButtonBox, QDoubleSpinBox, QPushButton,
QColorDialog)
from PyQt6.QtGui import QColor
from reportlab.lib.units import mm from reportlab.lib.units import mm
@@ -11,6 +13,7 @@ class PageNumbersDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Numerotation de pages") self.setWindowTitle("Numerotation de pages")
self.setMinimumWidth(400) self.setMinimumWidth(400)
self._color = QColor(0, 0, 0)
self._setup_ui() self._setup_ui()
def _setup_ui(self): def _setup_ui(self):
@@ -45,6 +48,17 @@ class PageNumbersDialog(QDialog):
fl.addWidget(self.spin_size) fl.addWidget(self.spin_size)
layout.addWidget(grp_font) layout.addWidget(grp_font)
# Couleur
color_layout = QHBoxLayout()
color_layout.addWidget(QLabel("Couleur :"))
self.btn_color = QPushButton()
self.btn_color.setFixedSize(60, 28)
self._update_color_button()
self.btn_color.clicked.connect(self._pick_color)
color_layout.addWidget(self.btn_color)
color_layout.addStretch()
layout.addLayout(color_layout)
# Texte # Texte
grp_text = QGroupBox("Texte") grp_text = QGroupBox("Texte")
tl = QHBoxLayout(grp_text) tl = QHBoxLayout(grp_text)
@@ -100,6 +114,17 @@ class PageNumbersDialog(QDialog):
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
layout.addWidget(buttons) layout.addWidget(buttons)
def _pick_color(self):
color = QColorDialog.getColor(self._color, self, "Couleur de la numerotation")
if color.isValid():
self._color = color
self._update_color_button()
def _update_color_button(self):
self.btn_color.setStyleSheet(
f"background-color: {self._color.name()}; border: 1px solid #999; border-radius: 4px;"
)
def get_settings(self): def get_settings(self):
apply_map = { apply_map = {
"Toutes les pages": "all", "Toutes les pages": "all",
@@ -110,7 +135,7 @@ class PageNumbersDialog(QDialog):
"position": self.combo_pos.currentText(), "position": self.combo_pos.currentText(),
"font_name": self.combo_font.currentText(), "font_name": self.combo_font.currentText(),
"font_size": self.spin_size.value(), "font_size": self.spin_size.value(),
"color": (0, 0, 0), "color": (self._color.redF(), self._color.greenF(), self._color.blueF()),
"prefix": self.edit_prefix.text(), "prefix": self.edit_prefix.text(),
"suffix": self.edit_suffix.text(), "suffix": self.edit_suffix.text(),
"start_number": self.spin_start.value(), "start_number": self.spin_start.value(),

View File

@@ -1,4 +1,4 @@
"""Dialogue Trim & Shift.""" """Dialogue Decalage du contenu."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QDoubleSpinBox, QCheckBox, QGroupBox, QDoubleSpinBox, QCheckBox,
@@ -9,7 +9,7 @@ from app.engine.pdf_utils import mm_to_pt
class TrimShiftDialog(QDialog): class TrimShiftDialog(QDialog):
def __init__(self, total_pages=1, parent=None): def __init__(self, total_pages=1, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Trim && Shift — Decaler le contenu") self.setWindowTitle("Decalage du contenu")
self.setMinimumWidth(420) self.setMinimumWidth(420)
self._total_pages = total_pages self._total_pages = total_pages
self._setup_ui() self._setup_ui()
@@ -55,10 +55,10 @@ class TrimShiftDialog(QDialog):
self.chk_different.toggled.connect(self.spin_x_even.setEnabled) self.chk_different.toggled.connect(self.spin_x_even.setEnabled)
self.chk_different.toggled.connect(self.spin_y_even.setEnabled) self.chk_different.toggled.connect(self.spin_y_even.setEnabled)
# Creep # Chasse (creep)
grp_creep = QGroupBox("Compensation de creep (livrets)") grp_creep = QGroupBox("Compensation de chasse (livrets)")
cl = QHBoxLayout(grp_creep) cl = QHBoxLayout(grp_creep)
cl.addWidget(QLabel("Creep max :")) cl.addWidget(QLabel("Chasse max :"))
self.spin_creep = QDoubleSpinBox() self.spin_creep = QDoubleSpinBox()
self.spin_creep.setRange(0, 20) self.spin_creep.setRange(0, 20)
self.spin_creep.setSuffix(" mm") self.spin_creep.setSuffix(" mm")

View File

@@ -5,36 +5,50 @@ from .pdf_utils import get_page_size
def crop_pages(input_path, output_path, crop_top=0, crop_bottom=0, def crop_pages(input_path, output_path, crop_top=0, crop_bottom=0,
crop_left=0, crop_right=0, pages=None): crop_left=0, crop_right=0, pages=None, per_page_settings=None):
"""Recadrer les pages specifiees. """Recadrer les pages specifiees.
crop_top/bottom/left/right : en points, quantite a retirer de chaque cote crop_top/bottom/left/right : en points, quantite a retirer de chaque cote
pages : liste d'indices (0-based) des pages a recadrer. None = toutes. pages : liste d'indices (0-based) des pages a recadrer. None = toutes.
per_page_settings : dict {page_index: {"crop_top", "crop_bottom", "crop_left", "crop_right"}}
Si fourni, applique des reglages differents par page.
""" """
reader = PdfReader(input_path) reader = PdfReader(input_path)
writer = PdfWriter() writer = PdfWriter()
if pages is None: if pages is None and per_page_settings is None:
pages = list(range(len(reader.pages))) pages = list(range(len(reader.pages)))
for i, page in enumerate(reader.pages): for i, page in enumerate(reader.pages):
if i in pages: # Verifier si on a des reglages specifiques pour cette page
w, h = get_page_size(page) if per_page_settings and i in per_page_settings:
new_left = float(page.mediabox.left) + crop_left ps = per_page_settings[i]
new_bottom = float(page.mediabox.bottom) + crop_bottom ct = ps.get("crop_top", 0)
new_right = float(page.mediabox.right) - crop_right cb = ps.get("crop_bottom", 0)
new_top = float(page.mediabox.top) - crop_top cl = ps.get("crop_left", 0)
cr = ps.get("crop_right", 0)
elif pages is not None and i in pages:
ct, cb, cl, cr = crop_top, crop_bottom, crop_left, crop_right
else:
writer.add_page(page)
continue
# Securite w, h = get_page_size(page)
if new_right <= new_left or new_top <= new_bottom: new_left = float(page.mediabox.left) + cl
writer.add_page(page) new_bottom = float(page.mediabox.bottom) + cb
continue new_right = float(page.mediabox.right) - cr
new_top = float(page.mediabox.top) - ct
page.mediabox.lower_left = (new_left, new_bottom) # Securite
page.mediabox.upper_right = (new_right, new_top) if new_right <= new_left or new_top <= new_bottom:
page.cropbox = page.mediabox writer.add_page(page)
if hasattr(page, 'trimbox'): continue
page.trimbox = page.mediabox
page.mediabox.lower_left = (new_left, new_bottom)
page.mediabox.upper_right = (new_right, new_top)
page.cropbox = page.mediabox
if hasattr(page, 'trimbox'):
page.trimbox = page.mediabox
writer.add_page(page) writer.add_page(page)

View File

@@ -1,6 +1,6 @@
"""Moteur Reperes de coupe autonomes.""" """Moteur Reperes de coupe autonomes."""
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from .pdf_utils import get_page_size from .pdf_utils import get_page_size
import io import io
@@ -8,11 +8,18 @@ import io
def add_crop_marks(input_path, output_path, mark_length=14.17, mark_offset=7.09, def add_crop_marks(input_path, output_path, mark_length=14.17, mark_offset=7.09,
line_width=0.25, color=(0, 0, 0), center_marks=False, line_width=0.25, color=(0, 0, 0), center_marks=False,
apply_to="all", current_page=0): apply_to="all", current_page=0, **_kwargs):
"""Ajouter des reperes de coupe sur les pages.""" """Ajouter des reperes de coupe sur les pages.
Les marques sont placees a l'exterieur de la zone d'impression.
Chaque coin a 2 traits (1 horizontal + 1 vertical) formant un L.
La page est agrandie pour contenir les marques.
"""
reader = PdfReader(input_path) reader = PdfReader(input_path)
writer = PdfWriter() writer = PdfWriter()
margin = mark_offset + mark_length
for i, page in enumerate(reader.pages): for i, page in enumerate(reader.pages):
page_num = i + 1 page_num = i + 1
should_apply = False should_apply = False
@@ -27,46 +34,62 @@ def add_crop_marks(input_path, output_path, mark_length=14.17, mark_offset=7.09,
if should_apply: if should_apply:
w, h = get_page_size(page) w, h = get_page_size(page)
# Canvas agrandi pour inclure les marques
cw = w + 2 * margin
ch = h + 2 * margin
packet = io.BytesIO() packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(w, h)) c = canvas.Canvas(packet, pagesize=(cw, ch))
c.setLineWidth(line_width) c.setLineWidth(line_width)
c.setStrokeColorRGB(*color) c.setStrokeColorRGB(*color)
# 4 coins # Les coordonnees de la zone d'impression dans le canvas agrandi
corners = [(0, 0), (w, 0), (0, h), (w, h)] ox, oy = margin, margin # origine de la page originale
for cx, cy in corners:
# Traits horizontaux # 4 coins — 2 traits par coin (L exterieur)
if cx == 0: # Bas-gauche
c.line(cx - mark_offset - mark_length, cy, cx - mark_offset, cy) c.line(ox - mark_offset - mark_length, oy, ox - mark_offset, oy)
else: c.line(ox, oy - mark_offset - mark_length, ox, oy - mark_offset)
c.line(cx + mark_offset, cy, cx + mark_offset + mark_length, cy) # Bas-droite
# Traits verticaux c.line(ox + w + mark_offset, oy, ox + w + mark_offset + mark_length, oy)
if cy == 0: c.line(ox + w, oy - mark_offset - mark_length, ox + w, oy - mark_offset)
c.line(cx, cy - mark_offset - mark_length, cx, cy - mark_offset) # Haut-gauche
else: c.line(ox - mark_offset - mark_length, oy + h, ox - mark_offset, oy + h)
c.line(cx, cy + mark_offset, cx, cy + mark_offset + mark_length) c.line(ox, oy + h + mark_offset, ox, oy + h + mark_offset + mark_length)
# Haut-droite
c.line(ox + w + mark_offset, oy + h, ox + w + mark_offset + mark_length, oy + h)
c.line(ox + w, oy + h + mark_offset, ox + w, oy + h + mark_offset + mark_length)
# Reperes de centrage # Reperes de centrage
if center_marks: if center_marks:
mid_x = w / 2 mid_x = ox + w / 2
mid_y = h / 2 mid_y = oy + h / 2
ml = mark_length * 0.7 ml = mark_length * 0.7
# Haut # Haut
c.line(mid_x, h + mark_offset, mid_x, h + mark_offset + ml) c.line(mid_x, oy + h + mark_offset, mid_x, oy + h + mark_offset + ml)
# Bas # Bas
c.line(mid_x, -mark_offset, mid_x, -mark_offset - ml) c.line(mid_x, oy - mark_offset, mid_x, oy - mark_offset - ml)
# Gauche # Gauche
c.line(-mark_offset, mid_y, -mark_offset - ml, mid_y) c.line(ox - mark_offset, mid_y, ox - mark_offset - ml, mid_y)
# Droite # Droite
c.line(w + mark_offset, mid_y, w + mark_offset + ml, mid_y) c.line(ox + w + mark_offset, mid_y, ox + w + mark_offset + ml, mid_y)
c.showPage() c.showPage()
c.save() c.save()
packet.seek(0) packet.seek(0)
overlay = PdfReader(packet) overlay_reader = PdfReader(packet)
page.merge_page(overlay.pages[0]) overlay_page = overlay_reader.pages[0]
writer.add_page(page) # Creer une nouvelle page agrandie
new_page = PageObject.create_blank_page(width=cw, height=ch)
# Placer la page originale au centre (decalee de margin)
page.add_transformation(Transformation().translate(margin, margin))
new_page.merge_page(page)
# Ajouter les marques de coupe par-dessus
new_page.merge_page(overlay_page)
writer.add_page(new_page)
else:
writer.add_page(page)
with open(output_path, "wb") as f: with open(output_path, "wb") as f:
writer.write(f) writer.write(f)

39
app/engine/disassemble.py Normal file
View File

@@ -0,0 +1,39 @@
"""Moteur Demonter les pages — couper en 2 sans reordonner."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size
def disassemble_pages(input_path, output_path):
"""Demonter un PDF : couper chaque page en 2 moities (gauche puis droite).
Contrairement a Delivretiser, les pages ne sont PAS remises dans l'ordre
du livret. On obtient simplement 2x plus de pages dans l'ordre de lecture.
"""
reader = PdfReader(input_path)
writer = PdfWriter()
for page in reader.pages:
w, h = get_page_size(page)
half_w = w / 2
# Moitie gauche
left_page = PageObject.create_blank_page(width=half_w, height=h)
left_overlay = PageObject.create_blank_page(width=half_w, height=h)
left_overlay.merge_page(page)
left_overlay.mediabox.upper_right = (half_w, h)
left_page.merge_page(left_overlay)
writer.add_page(left_page)
# Moitie droite
right_page = PageObject.create_blank_page(width=half_w, height=h)
right_overlay = PageObject.create_blank_page(width=half_w, height=h)
right_overlay.merge_page(page)
right_overlay.add_transformation(Transformation().translate(-half_w, 0))
right_overlay.mediabox.upper_right = (half_w, h)
right_page.merge_page(right_overlay)
writer.add_page(right_page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

View File

@@ -10,7 +10,8 @@ def apply_masking(input_path, output_path, rectangles, apply_to="all"):
"""Appliquer des rectangles blancs de masquage. """Appliquer des rectangles blancs de masquage.
rectangles: liste de dicts {"x": pt, "y": pt, "width": pt, "height": pt} rectangles: liste de dicts {"x": pt, "y": pt, "width": pt, "height": pt}
apply_to: "all", "even", "odd", ou un numero de page (1-based) apply_to: "all", "even", "odd", un numero de page (int, 1-based),
ou une liste d'indices (0-based)
""" """
reader = PdfReader(input_path) reader = PdfReader(input_path)
writer = PdfWriter() writer = PdfWriter()
@@ -27,6 +28,8 @@ def apply_masking(input_path, output_path, rectangles, apply_to="all"):
should_apply = page_num % 2 != 0 should_apply = page_num % 2 != 0
elif isinstance(apply_to, int): elif isinstance(apply_to, int):
should_apply = page_num == apply_to should_apply = page_num == apply_to
elif isinstance(apply_to, list):
should_apply = i in apply_to
if should_apply and rectangles: if should_apply and rectangles:
w, h = get_page_size(page) w, h = get_page_size(page)

View File

@@ -40,6 +40,7 @@ from app.engine.crop_marks import add_crop_marks, remove_crop_marks
from app.engine.crop import crop_pages from app.engine.crop import crop_pages
from app.engine.add_content import add_text_to_pages, add_image_to_pages from app.engine.add_content import add_text_to_pages, add_image_to_pages
from app.engine.unbooklet import unbooklet from app.engine.unbooklet import unbooklet
from app.engine.disassemble import disassemble_pages
from app.engine.trim_shift import trim_and_shift 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.engine.extract_content import extract_text_from_page, extract_images_from_page
from app.dialogs.trim_shift_dialog import TrimShiftDialog from app.dialogs.trim_shift_dialog import TrimShiftDialog
@@ -63,6 +64,7 @@ class MainWindow(QMainWindow):
self._result_file = None self._result_file = None
self._original_file = None self._original_file = None
self._crop_settings = {} self._crop_settings = {}
self._bleed_settings = None
self._undo_stack = [] # pile de fichiers pour annuler self._undo_stack = [] # pile de fichiers pour annuler
self._redo_stack = [] self._redo_stack = []
@@ -221,11 +223,12 @@ class MainWindow(QMainWindow):
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages) self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
impose_menu.addSeparator() impose_menu.addSeparator()
self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet) self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet)
self._add_action(impose_menu, "Demonter les pages", None, self._do_disassemble)
# Pages # Pages
pages_menu = menubar.addMenu("Pages") pages_menu = menubar.addMenu("Pages")
self._add_action(pages_menu, "Recadrer...", None, self._do_crop) self._add_action(pages_menu, "Recadrer...", None, self._do_crop)
self._add_action(pages_menu, "Trim && Shift...", None, self._do_trim_shift) self._add_action(pages_menu, "Decalage...", None, self._do_trim_shift)
pages_menu.addSeparator() pages_menu.addSeparator()
self._add_action(pages_menu, "Pivoter +90", None, lambda: self._rotate_current(90)) self._add_action(pages_menu, "Pivoter +90", None, lambda: self._rotate_current(90))
self._add_action(pages_menu, "Pivoter -90", None, lambda: self._rotate_current(-90)) self._add_action(pages_menu, "Pivoter -90", None, lambda: self._rotate_current(-90))
@@ -236,6 +239,13 @@ class MainWindow(QMainWindow):
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank) self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
self._add_action(pages_menu, "Creer un document test...", None, self._do_create_sample) self._add_action(pages_menu, "Creer un document test...", None, self._do_create_sample)
# Affichage
view_menu = menubar.addMenu("Affichage")
self._rulers_action = QAction("Afficher les regles", self)
self._rulers_action.setCheckable(True)
self._rulers_action.toggled.connect(self._toggle_rulers)
view_menu.addAction(self._rulers_action)
# Annotations # Annotations
annot_menu = menubar.addMenu("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 du texte...", None, self._do_add_text)
@@ -319,6 +329,7 @@ class MainWindow(QMainWindow):
rtb.addWidget(self._make_section_label("IMPOSITION")) rtb.addWidget(self._make_section_label("IMPOSITION"))
rtb.addAction("Livret", self._do_booklet) rtb.addAction("Livret", self._do_booklet)
rtb.addAction("Delivretiser", self._do_unbooklet) rtb.addAction("Delivretiser", self._do_unbooklet)
rtb.addAction("Demonter", self._do_disassemble)
rtb.addAction("N-Up", self._do_nup) rtb.addAction("N-Up", self._do_nup)
rtb.addAction("Repetition", self._do_step_repeat) rtb.addAction("Repetition", self._do_step_repeat)
rtb.addAction("Manuel", self._do_manual_impose) rtb.addAction("Manuel", self._do_manual_impose)
@@ -328,7 +339,7 @@ class MainWindow(QMainWindow):
# Section Pages # Section Pages
rtb.addWidget(self._make_section_label("PAGES")) rtb.addWidget(self._make_section_label("PAGES"))
rtb.addAction("Recadrer", self._do_crop) rtb.addAction("Recadrer", self._do_crop)
rtb.addAction("Trim/Shift", self._do_trim_shift) rtb.addAction("Decalage", self._do_trim_shift)
rtb.addAction("Pg blanche", self._do_insert_blank) rtb.addAction("Pg blanche", self._do_insert_blank)
rtb.addAction("Inverser", self._do_reverse_pages) rtb.addAction("Inverser", self._do_reverse_pages)
rtb.addAction("Fusionner", self._do_merge) rtb.addAction("Fusionner", self._do_merge)
@@ -633,6 +644,22 @@ class MainWindow(QMainWindow):
self._hide_progress() self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e)) QMessageBox.critical(self, "Erreur", str(e))
def _do_disassemble(self):
if not self._require_file():
return
self._show_progress()
output = self._make_temp()
try:
disassemble_pages(self._current_file, output)
self._show_result(output)
self._status_label.setText("Demontage termine — pages coupees en 2")
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _toggle_rulers(self, checked):
self._viewer.set_rulers_visible(checked)
def _do_trim_shift(self): def _do_trim_shift(self):
if not self._require_file(): if not self._require_file():
return return
@@ -768,18 +795,51 @@ class MainWindow(QMainWindow):
def _do_masking(self): def _do_masking(self):
if not self._require_file(): if not self._require_file():
return return
dlg = MaskingDialog(self) from pypdf import PdfReader
if dlg.exec(): from app.engine.pdf_utils import get_page_size
reader = PdfReader(self._current_file)
cur_page = self._viewer.current_page
page = reader.pages[cur_page]
w, h = get_page_size(page)
self._mask_dlg = MaskingDialog(
page_width_pt=w, page_height_pt=h,
total_pages=len(reader.pages),
current_page=cur_page,
viewer=self._viewer,
parent=self
)
def on_mask_accepted():
if not hasattr(self, '_mask_dlg') or not self._mask_dlg:
return
dlg_ref = self._mask_dlg
self._mask_dlg = None
try:
settings = dlg_ref.get_settings()
except Exception:
return
if not settings.get("rectangles"):
return
self._show_progress() self._show_progress()
settings = dlg.get_settings()
output = self._make_temp() output = self._make_temp()
try: try:
apply_masking(self._current_file, output, **settings) apply_masking(self._current_file, output, **settings)
self._show_result(output) self._show_result(output)
self._status_label.setText(
f"Masquage applique — {len(settings['rectangles'])} zone(s)"
)
except Exception as e: except Exception as e:
self._hide_progress() self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e)) QMessageBox.critical(self, "Erreur", str(e))
def on_mask_rejected():
self._mask_dlg = None
self._mask_dlg.accepted.connect(on_mask_accepted)
self._mask_dlg.rejected.connect(on_mask_rejected)
self._mask_dlg.show()
def _do_bleeds(self): def _do_bleeds(self):
if not self._require_file(): if not self._require_file():
return return
@@ -791,6 +851,18 @@ class MainWindow(QMainWindow):
try: try:
define_bleeds(self._current_file, output, **settings) define_bleeds(self._current_file, output, **settings)
self._show_result(output) self._show_result(output)
# Stocker et afficher l'overlay rouge sur le viewer
from app.engine.pdf_utils import mm_to_pt
self._bleed_settings = {
"top": mm_to_pt(settings["bleed_top"]),
"bottom": mm_to_pt(settings["bleed_bottom"]),
"left": mm_to_pt(settings["bleed_left"]),
"right": mm_to_pt(settings["bleed_right"]),
}
self._viewer.set_bleed_info(self._bleed_settings)
self._status_label.setText(
f"Fonds perdus definis — zone rouge = bleed"
)
except Exception as e: except Exception as e:
self._hide_progress() self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e)) QMessageBox.critical(self, "Erreur", str(e))
@@ -804,15 +876,20 @@ class MainWindow(QMainWindow):
reader = PdfReader(self._current_file) reader = PdfReader(self._current_file)
writer = PdfWriter() writer = PdfWriter()
for page in reader.pages: for page in reader.pages:
if hasattr(page, 'trimbox'): # Remettre trimbox et bleedbox a la taille de la mediabox
page.trimbox = page.mediabox page.trimbox.lower_left = page.mediabox.lower_left
if hasattr(page, 'bleedbox'): page.trimbox.upper_right = page.mediabox.upper_right
page.bleedbox = page.mediabox page.bleedbox.lower_left = page.mediabox.lower_left
page.bleedbox.upper_right = page.mediabox.upper_right
writer.add_page(page) writer.add_page(page)
output = self._make_temp() output = self._make_temp()
with open(output, "wb") as f: with open(output, "wb") as f:
writer.write(f) writer.write(f)
self._show_result(output) self._show_result(output)
# Supprimer l'overlay rouge
self._bleed_settings = None
self._viewer.set_bleed_info(None)
self._status_label.setText("Fonds perdus supprimes")
except Exception as e: except Exception as e:
self._hide_progress() self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e)) QMessageBox.critical(self, "Erreur", str(e))
@@ -976,20 +1053,21 @@ class MainWindow(QMainWindow):
"crop_right": settings["crop_right"], "crop_right": settings["crop_right"],
} }
# Appliquer le crop depuis l'original # Appliquer TOUS les crops cumules depuis l'original
self._show_progress() self._show_progress()
source_file = self._original_file or self._current_file source_file = self._original_file or self._current_file
output = self._make_temp() output = self._make_temp()
try: try:
crop_pages(source_file, output, **settings) crop_pages(source_file, output, per_page_settings=self._crop_settings)
self._result_file = output self._result_file = output
self._current_file = output self._current_file = output
self._viewer.load_pdf(output) self._viewer.load_pdf(output)
self._viewer.go_to_page(cur_page) self._viewer.go_to_page(cur_page)
self._page_panel.load_thumbnails(self._viewer) self._page_panel.load_thumbnails(self._viewer)
self._hide_progress() self._hide_progress()
total_cropped = len(self._crop_settings)
self._status_label.setText( self._status_label.setText(
f"Recadrage applique — {len(settings['pages'])} page(s)" f"Recadrage applique — {total_cropped} page(s) recadree(s) au total"
) )
QTimer.singleShot(100, self._viewer.zoom_fit) QTimer.singleShot(100, self._viewer.zoom_fit)
except Exception as e: except Exception as e:

View File

@@ -1,13 +1,14 @@
"""Widget de visualisation PDF avec pypdfium2 — V2.5 avec selection redimensionnable.""" """Widget de visualisation PDF avec pypdfium2 — V3 avec selection + regles."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QLabel, from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QScrollArea, QSizePolicy, QApplication) QScrollArea, QSizePolicy, QApplication)
from PyQt6.QtGui import (QImage, QPixmap, QWheelEvent, QPainter, QPen, QColor, from PyQt6.QtGui import (QImage, QPixmap, QWheelEvent, QPainter, QPen, QColor,
QMouseEvent, QCursor, QBrush) QMouseEvent, QCursor, QBrush, QFont)
from PyQt6.QtCore import Qt, pyqtSignal, QRect, QPoint, QSize from PyQt6.QtCore import Qt, pyqtSignal, QRect, QPoint, QSize
import pypdfium2 as pdfium import pypdfium2 as pdfium
HANDLE_SIZE = 8 # taille des poignees de redimensionnement HANDLE_SIZE = 8 # taille des poignees de redimensionnement
RULER_SIZE = 24 # largeur/hauteur des regles en pixels
class SelectableImageLabel(QLabel): class SelectableImageLabel(QLabel):
@@ -231,8 +232,105 @@ class SelectableImageLabel(QLabel):
painter.end() painter.end()
class RulerWidget(QWidget):
"""Regle graduee en mm (horizontale ou verticale)."""
def __init__(self, orientation=Qt.Orientation.Horizontal, parent=None):
super().__init__(parent)
self._orientation = orientation
self._page_size_pt = 0 # taille de la page en points
self._zoom = 1.0
self._offset = 0 # decalage du scroll
self._img_offset = 0 # offset de l'image dans le label
if orientation == Qt.Orientation.Horizontal:
self.setFixedHeight(RULER_SIZE)
else:
self.setFixedWidth(RULER_SIZE)
def update_params(self, page_size_pt, zoom, offset, img_offset):
self._page_size_pt = page_size_pt
self._zoom = zoom
self._offset = offset
self._img_offset = img_offset
self.update()
def paintEvent(self, event):
if self._page_size_pt <= 0:
return
painter = QPainter(self)
painter.fillRect(self.rect(), QColor(245, 245, 245))
pen = QPen(QColor(120, 120, 120), 1)
painter.setPen(pen)
font = QFont("Segoe UI", 7)
painter.setFont(font)
# pixels par mm
px_per_pt = 150 / 72 * self._zoom
px_per_mm = px_per_pt * (72 / 25.4)
page_size_mm = self._page_size_pt * 25.4 / 72
# Position de depart de la page dans le widget
page_start = self._img_offset - self._offset
horiz = self._orientation == Qt.Orientation.Horizontal
# Determiner le pas des graduations
if px_per_mm >= 4:
step_mm = 5
sub_step_mm = 1
elif px_per_mm >= 2:
step_mm = 10
sub_step_mm = 5
else:
step_mm = 20
sub_step_mm = 10
# Dessiner les graduations
mm_val = 0
while mm_val <= page_size_mm:
px = page_start + mm_val * px_per_mm
if horiz:
if px < 0 or px > self.width():
mm_val += sub_step_mm
continue
else:
if px < 0 or px > self.height():
mm_val += sub_step_mm
continue
is_major = (mm_val % step_mm == 0)
tick_len = RULER_SIZE * 0.6 if is_major else RULER_SIZE * 0.3
if horiz:
x = int(px)
painter.drawLine(x, RULER_SIZE - int(tick_len), x, RULER_SIZE)
if is_major and mm_val > 0:
painter.drawText(x + 2, RULER_SIZE - int(tick_len) - 1, str(int(mm_val)))
else:
y = int(px)
painter.drawLine(RULER_SIZE - int(tick_len), y, RULER_SIZE, y)
if is_major and mm_val > 0:
painter.save()
painter.translate(RULER_SIZE - int(tick_len) - 2, y + 2)
painter.rotate(-90)
painter.drawText(0, 0, str(int(mm_val)))
painter.restore()
mm_val += sub_step_mm
# Ligne de bord
if horiz:
painter.drawLine(0, RULER_SIZE - 1, self.width(), RULER_SIZE - 1)
else:
painter.drawLine(RULER_SIZE - 1, 0, RULER_SIZE - 1, self.height())
painter.end()
class PdfViewer(QWidget): class PdfViewer(QWidget):
"""Widget d'apercu PDF avec zoom molette, navigation et selection.""" """Widget d'apercu PDF avec zoom molette, navigation, selection et regles."""
page_changed = pyqtSignal(int, int) page_changed = pyqtSignal(int, int)
selection_changed = pyqtSignal(float, float, float, float) selection_changed = pyqtSignal(float, float, float, float)
@@ -247,6 +345,8 @@ class PdfViewer(QWidget):
self._zoom = 1.0 self._zoom = 1.0
self._page_width_pt = 0 self._page_width_pt = 0
self._page_height_pt = 0 self._page_height_pt = 0
self._rulers_visible = False
self._bleed_info = None # dict avec top/bottom/left/right en pt
self._setup_ui() self._setup_ui()
def _setup_ui(self): def _setup_ui(self):
@@ -254,6 +354,18 @@ class PdfViewer(QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0) layout.setSpacing(0)
# Regle horizontale
self._h_ruler = RulerWidget(Qt.Orientation.Horizontal)
self._h_ruler.setVisible(False)
# Zone centrale : regle verticale + scroll
center_layout = QHBoxLayout()
center_layout.setContentsMargins(0, 0, 0, 0)
center_layout.setSpacing(0)
self._v_ruler = RulerWidget(Qt.Orientation.Vertical)
self._v_ruler.setVisible(False)
self._scroll = QScrollArea() self._scroll = QScrollArea()
self._scroll.setWidgetResizable(True) self._scroll.setWidgetResizable(True)
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter) self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -268,7 +380,41 @@ class PdfViewer(QWidget):
self._scroll.setWidget(self._image_label) self._scroll.setWidget(self._image_label)
self._scroll.wheelEvent = self._on_wheel self._scroll.wheelEvent = self._on_wheel
layout.addWidget(self._scroll, 1) self._scroll.horizontalScrollBar().valueChanged.connect(self._update_rulers)
self._scroll.verticalScrollBar().valueChanged.connect(self._update_rulers)
center_layout.addWidget(self._v_ruler)
center_layout.addWidget(self._scroll, 1)
# Coin superieur gauche (vide) quand regles actives
h_ruler_layout = QHBoxLayout()
h_ruler_layout.setContentsMargins(0, 0, 0, 0)
h_ruler_layout.setSpacing(0)
self._ruler_corner = QWidget()
self._ruler_corner.setFixedSize(RULER_SIZE, RULER_SIZE)
self._ruler_corner.setStyleSheet("background: #f5f5f5;")
self._ruler_corner.setVisible(False)
h_ruler_layout.addWidget(self._ruler_corner)
h_ruler_layout.addWidget(self._h_ruler, 1)
layout.addLayout(h_ruler_layout)
layout.addLayout(center_layout)
def set_rulers_visible(self, visible):
self._rulers_visible = visible
self._h_ruler.setVisible(visible)
self._v_ruler.setVisible(visible)
self._ruler_corner.setVisible(visible)
if visible:
self._update_rulers()
def rulers_visible(self):
return self._rulers_visible
def set_bleed_info(self, bleed_info):
"""Definir les infos de bleed pour l'overlay rouge. None pour desactiver."""
self._bleed_info = bleed_info
self._render_page()
def set_select_mode(self, enabled): def set_select_mode(self, enabled):
self._image_label.set_select_mode(enabled) self._image_label.set_select_mode(enabled)
@@ -279,6 +425,25 @@ class PdfViewer(QWidget):
def show_selection(self, left_ratio, top_ratio, right_ratio, bottom_ratio): def show_selection(self, left_ratio, top_ratio, right_ratio, bottom_ratio):
self._image_label.set_selection_from_margins(left_ratio, top_ratio, right_ratio, bottom_ratio) self._image_label.set_selection_from_margins(left_ratio, top_ratio, right_ratio, bottom_ratio)
def _update_rulers(self):
if not self._rulers_visible or not self._pdf:
return
# Calculer l'offset de l'image dans le scroll area
pm = self._image_label.pixmap()
if not pm:
return
img_w, img_h = pm.width(), pm.height()
label_w = self._image_label.width()
label_h = self._image_label.height()
img_off_x = max(0, (label_w - img_w) // 2)
img_off_y = max(0, (label_h - img_h) // 2)
scroll_x = self._scroll.horizontalScrollBar().value()
scroll_y = self._scroll.verticalScrollBar().value()
self._h_ruler.update_params(self._page_width_pt, self._zoom, scroll_x, img_off_x)
self._v_ruler.update_params(self._page_height_pt, self._zoom, scroll_y, img_off_y)
def _on_wheel(self, event): def _on_wheel(self, event):
if event.modifiers() & Qt.KeyboardModifier.ControlModifier: if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
delta = event.angleDelta().y() delta = event.angleDelta().y()
@@ -324,11 +489,43 @@ class PdfViewer(QWidget):
pil_image = bitmap.to_pil() pil_image = bitmap.to_pil()
data = pil_image.tobytes("raw", "RGBA") data = pil_image.tobytes("raw", "RGBA")
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888) qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
# Overlay bleed si actif
if self._bleed_info:
from PyQt6.QtGui import QPainter as _P
px_per_pt = scale
bt = self._bleed_info.get("top", 0) * px_per_pt
bb = self._bleed_info.get("bottom", 0) * px_per_pt
bl = self._bleed_info.get("left", 0) * px_per_pt
br = self._bleed_info.get("right", 0) * px_per_pt
img_w = qimage.width()
img_h = qimage.height()
painter = _P(qimage)
painter.setBrush(QColor(255, 0, 0, 50))
painter.setPen(QPen(QColor(255, 0, 0, 120), 1, Qt.PenStyle.DashLine))
# Haut
if bt > 0:
painter.drawRect(0, 0, img_w, int(bt))
# Bas
if bb > 0:
painter.drawRect(0, img_h - int(bb), img_w, int(bb))
# Gauche
if bl > 0:
painter.drawRect(0, int(bt), int(bl), img_h - int(bt) - int(bb))
# Droite
if br > 0:
painter.drawRect(img_w - int(br), int(bt), int(br), img_h - int(bt) - int(bb))
painter.end()
pixmap = QPixmap.fromImage(qimage) pixmap = QPixmap.fromImage(qimage)
self._image_label.setPixmap(pixmap) self._image_label.setPixmap(pixmap)
self._image_label.adjustSize() self._image_label.adjustSize()
self.page_changed.emit(self._current_page, self._total_pages) self.page_changed.emit(self._current_page, self._total_pages)
if self._rulers_visible:
self._update_rulers()
def _zoom_fit(self): def _zoom_fit(self):
if not self._pdf or self._current_page >= self._total_pages: if not self._pdf or self._current_page >= self._total_pages:
return return

View File

@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
BRANCH = "master" BRANCH = "master"
# Version actuelle (incrementee a chaque release) # Version actuelle (incrementee a chaque release)
VERSION = "3.5.0" VERSION = "3.6.0"
def get_remote_version(): def get_remote_version():

View File

@@ -1 +1 @@
3.5.0 3.6.0