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:
@@ -64,12 +64,18 @@ class CustomSizeDialog(QDialog):
|
||||
self.spin_w.setRange(10, 5000)
|
||||
self.spin_w.setValue(210)
|
||||
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(QLabel("Hauteur :"))
|
||||
self.spin_h = QDoubleSpinBox()
|
||||
self.spin_h.setRange(10, 5000)
|
||||
self.spin_h.setValue(297)
|
||||
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)
|
||||
layout.addWidget(grp)
|
||||
|
||||
|
||||
@@ -12,10 +12,27 @@ ADD_LABEL = "Ajouter un format..."
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
for name in PAGE_SIZES:
|
||||
combo.addItem(name, PAGE_SIZES[name])
|
||||
|
||||
@@ -1,61 +1,109 @@
|
||||
"""Dialogue Masquage (rectangles blancs)."""
|
||||
"""Dialogue Masquage (rectangles blancs) — avec selection visuelle."""
|
||||
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QGroupBox, QComboBox, QDoubleSpinBox,
|
||||
QDialogButtonBox, QPushButton, QListWidget,
|
||||
QListWidgetItem)
|
||||
from app.engine.pdf_utils import mm_to_pt
|
||||
QLineEdit, QRadioButton)
|
||||
from PyQt6.QtCore import Qt
|
||||
from app.engine.pdf_utils import mm_to_pt, pt_to_mm
|
||||
|
||||
|
||||
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)
|
||||
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._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Definition rectangle
|
||||
grp_rect = QGroupBox("Definir un rectangle de masquage (mm)")
|
||||
rl = QHBoxLayout(grp_rect)
|
||||
# Info
|
||||
pw_mm = pt_to_mm(self._page_w)
|
||||
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.setRange(0, 1000)
|
||||
self.spin_x.setRange(0, 2000)
|
||||
self.spin_x.setSuffix(" mm")
|
||||
rl.addWidget(self.spin_x)
|
||||
|
||||
rl.addWidget(QLabel("Y:"))
|
||||
row1.addWidget(self.spin_x)
|
||||
row1.addWidget(QLabel("Y :"))
|
||||
self.spin_y = QDoubleSpinBox()
|
||||
self.spin_y.setRange(0, 1000)
|
||||
self.spin_y.setRange(0, 2000)
|
||||
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.setRange(1, 1000)
|
||||
self.spin_w.setRange(1, 2000)
|
||||
self.spin_w.setValue(50)
|
||||
self.spin_w.setSuffix(" mm")
|
||||
rl.addWidget(self.spin_w)
|
||||
|
||||
rl.addWidget(QLabel("H:"))
|
||||
row2.addWidget(self.spin_w)
|
||||
row2.addWidget(QLabel("Hauteur :"))
|
||||
self.spin_h = QDoubleSpinBox()
|
||||
self.spin_h.setRange(1, 1000)
|
||||
self.spin_h.setRange(1, 2000)
|
||||
self.spin_h.setValue(10)
|
||||
self.spin_h.setSuffix(" mm")
|
||||
rl.addWidget(self.spin_h)
|
||||
row2.addWidget(self.spin_h)
|
||||
rl.addLayout(row2)
|
||||
|
||||
layout.addWidget(grp_rect)
|
||||
|
||||
# Boutons ajouter/supprimer
|
||||
# Bouton ajouter
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_add = QPushButton("Ajouter")
|
||||
btn_add.clicked.connect(self._add_rect)
|
||||
btn_layout.addWidget(btn_add)
|
||||
self.btn_add = QPushButton("Ajouter ce masque")
|
||||
self.btn_add.setStyleSheet("""
|
||||
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.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_layout.addWidget(btn_del)
|
||||
btn_layout.addStretch()
|
||||
@@ -63,15 +111,31 @@ class MaskingDialog(QDialog):
|
||||
|
||||
# Liste des rectangles
|
||||
self.list_rects = QListWidget()
|
||||
self.list_rects.setMaximumHeight(100)
|
||||
layout.addWidget(self.list_rects)
|
||||
|
||||
# Appliquer a
|
||||
grp_apply = QGroupBox("Appliquer a")
|
||||
al = QHBoxLayout(grp_apply)
|
||||
self.combo_apply = QComboBox()
|
||||
self.combo_apply.addItems(["Toutes les pages", "Pages paires", "Pages impaires"])
|
||||
al.addWidget(self.combo_apply)
|
||||
al = QVBoxLayout(grp_apply)
|
||||
self.rb_current = QRadioButton(f"Page courante ({self._current_page + 1})")
|
||||
self.rb_current.setChecked(True)
|
||||
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)
|
||||
self.rb_range.toggled.connect(self.edit_range.setEnabled)
|
||||
|
||||
# Boutons
|
||||
buttons = QDialogButtonBox(
|
||||
@@ -81,32 +145,128 @@ class MaskingDialog(QDialog):
|
||||
buttons.rejected.connect(self.reject)
|
||||
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):
|
||||
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 = {
|
||||
"x": mm_to_pt(self.spin_x.value()),
|
||||
"y": mm_to_pt(self.spin_y.value()),
|
||||
"width": mm_to_pt(self.spin_w.value()),
|
||||
"height": mm_to_pt(self.spin_h.value()),
|
||||
"x": mm_to_pt(x_mm),
|
||||
"y": mm_to_pt(pdf_y_mm),
|
||||
"width": mm_to_pt(w_mm),
|
||||
"height": mm_to_pt(h_mm),
|
||||
}
|
||||
self._rectangles.append(rect)
|
||||
self.list_rects.addItem(
|
||||
f"X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} "
|
||||
f"L={self.spin_w.value():.1f} H={self.spin_h.value():.1f} mm"
|
||||
f"X={x_mm:.1f} Y={y_mm:.1f} L={w_mm:.1f} H={h_mm:.1f} mm"
|
||||
)
|
||||
|
||||
# Nettoyer la selection pour en dessiner une autre
|
||||
if self._viewer:
|
||||
self._viewer.clear_selection()
|
||||
|
||||
def _del_rect(self):
|
||||
row = self.list_rects.currentRow()
|
||||
if row >= 0:
|
||||
self.list_rects.takeItem(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):
|
||||
apply_map = {
|
||||
"Toutes les pages": "all",
|
||||
"Pages paires": "even",
|
||||
"Pages impaires": "odd"
|
||||
}
|
||||
if self._viewer:
|
||||
self._viewer.set_select_mode(False)
|
||||
self._viewer.clear_selection()
|
||||
|
||||
# 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 {
|
||||
"rectangles": self._rectangles,
|
||||
"apply_to": apply_map[self.combo_apply.currentText()],
|
||||
"apply_to": apply_to,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QGroupBox, QComboBox, QSpinBox, QLineEdit,
|
||||
QDialogButtonBox, QDoubleSpinBox)
|
||||
QDialogButtonBox, QDoubleSpinBox, QPushButton,
|
||||
QColorDialog)
|
||||
from PyQt6.QtGui import QColor
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
|
||||
@@ -11,6 +13,7 @@ class PageNumbersDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Numerotation de pages")
|
||||
self.setMinimumWidth(400)
|
||||
self._color = QColor(0, 0, 0)
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
@@ -45,6 +48,17 @@ class PageNumbersDialog(QDialog):
|
||||
fl.addWidget(self.spin_size)
|
||||
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
|
||||
grp_text = QGroupBox("Texte")
|
||||
tl = QHBoxLayout(grp_text)
|
||||
@@ -100,6 +114,17 @@ class PageNumbersDialog(QDialog):
|
||||
buttons.rejected.connect(self.reject)
|
||||
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):
|
||||
apply_map = {
|
||||
"Toutes les pages": "all",
|
||||
@@ -110,7 +135,7 @@ class PageNumbersDialog(QDialog):
|
||||
"position": self.combo_pos.currentText(),
|
||||
"font_name": self.combo_font.currentText(),
|
||||
"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(),
|
||||
"suffix": self.edit_suffix.text(),
|
||||
"start_number": self.spin_start.value(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Dialogue Trim & Shift."""
|
||||
"""Dialogue Decalage du contenu."""
|
||||
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QGroupBox, QDoubleSpinBox, QCheckBox,
|
||||
@@ -9,7 +9,7 @@ from app.engine.pdf_utils import mm_to_pt
|
||||
class TrimShiftDialog(QDialog):
|
||||
def __init__(self, total_pages=1, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Trim && Shift — Decaler le contenu")
|
||||
self.setWindowTitle("Decalage du contenu")
|
||||
self.setMinimumWidth(420)
|
||||
self._total_pages = total_pages
|
||||
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_y_even.setEnabled)
|
||||
|
||||
# Creep
|
||||
grp_creep = QGroupBox("Compensation de creep (livrets)")
|
||||
# Chasse (creep)
|
||||
grp_creep = QGroupBox("Compensation de chasse (livrets)")
|
||||
cl = QHBoxLayout(grp_creep)
|
||||
cl.addWidget(QLabel("Creep max :"))
|
||||
cl.addWidget(QLabel("Chasse max :"))
|
||||
self.spin_creep = QDoubleSpinBox()
|
||||
self.spin_creep.setRange(0, 20)
|
||||
self.spin_creep.setSuffix(" mm")
|
||||
|
||||
@@ -5,25 +5,39 @@ from .pdf_utils import get_page_size
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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)
|
||||
writer = PdfWriter()
|
||||
|
||||
if pages is None:
|
||||
if pages is None and per_page_settings is None:
|
||||
pages = list(range(len(reader.pages)))
|
||||
|
||||
for i, page in enumerate(reader.pages):
|
||||
if i in pages:
|
||||
# Verifier si on a des reglages specifiques pour cette page
|
||||
if per_page_settings and i in per_page_settings:
|
||||
ps = per_page_settings[i]
|
||||
ct = ps.get("crop_top", 0)
|
||||
cb = ps.get("crop_bottom", 0)
|
||||
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
|
||||
|
||||
w, h = get_page_size(page)
|
||||
new_left = float(page.mediabox.left) + crop_left
|
||||
new_bottom = float(page.mediabox.bottom) + crop_bottom
|
||||
new_right = float(page.mediabox.right) - crop_right
|
||||
new_top = float(page.mediabox.top) - crop_top
|
||||
new_left = float(page.mediabox.left) + cl
|
||||
new_bottom = float(page.mediabox.bottom) + cb
|
||||
new_right = float(page.mediabox.right) - cr
|
||||
new_top = float(page.mediabox.top) - ct
|
||||
|
||||
# Securite
|
||||
if new_right <= new_left or new_top <= new_bottom:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Moteur Reperes de coupe autonomes."""
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
|
||||
from reportlab.pdfgen import canvas
|
||||
from .pdf_utils import get_page_size
|
||||
import io
|
||||
@@ -8,11 +8,18 @@ import io
|
||||
|
||||
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,
|
||||
apply_to="all", current_page=0):
|
||||
"""Ajouter des reperes de coupe sur les pages."""
|
||||
apply_to="all", current_page=0, **_kwargs):
|
||||
"""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)
|
||||
writer = PdfWriter()
|
||||
|
||||
margin = mark_offset + mark_length
|
||||
|
||||
for i, page in enumerate(reader.pages):
|
||||
page_num = i + 1
|
||||
should_apply = False
|
||||
@@ -27,45 +34,61 @@ def add_crop_marks(input_path, output_path, mark_length=14.17, mark_offset=7.09,
|
||||
|
||||
if should_apply:
|
||||
w, h = get_page_size(page)
|
||||
# Canvas agrandi pour inclure les marques
|
||||
cw = w + 2 * margin
|
||||
ch = h + 2 * margin
|
||||
|
||||
packet = io.BytesIO()
|
||||
c = canvas.Canvas(packet, pagesize=(w, h))
|
||||
c = canvas.Canvas(packet, pagesize=(cw, ch))
|
||||
c.setLineWidth(line_width)
|
||||
c.setStrokeColorRGB(*color)
|
||||
|
||||
# 4 coins
|
||||
corners = [(0, 0), (w, 0), (0, h), (w, h)]
|
||||
for cx, cy in corners:
|
||||
# Traits horizontaux
|
||||
if cx == 0:
|
||||
c.line(cx - mark_offset - mark_length, cy, cx - mark_offset, cy)
|
||||
else:
|
||||
c.line(cx + mark_offset, cy, cx + mark_offset + mark_length, cy)
|
||||
# Traits verticaux
|
||||
if cy == 0:
|
||||
c.line(cx, cy - mark_offset - mark_length, cx, cy - mark_offset)
|
||||
else:
|
||||
c.line(cx, cy + mark_offset, cx, cy + mark_offset + mark_length)
|
||||
# Les coordonnees de la zone d'impression dans le canvas agrandi
|
||||
ox, oy = margin, margin # origine de la page originale
|
||||
|
||||
# 4 coins — 2 traits par coin (L exterieur)
|
||||
# Bas-gauche
|
||||
c.line(ox - mark_offset - mark_length, oy, ox - mark_offset, oy)
|
||||
c.line(ox, oy - mark_offset - mark_length, ox, oy - mark_offset)
|
||||
# Bas-droite
|
||||
c.line(ox + w + mark_offset, oy, ox + w + mark_offset + mark_length, oy)
|
||||
c.line(ox + w, oy - mark_offset - mark_length, ox + w, oy - mark_offset)
|
||||
# Haut-gauche
|
||||
c.line(ox - mark_offset - mark_length, oy + h, ox - mark_offset, oy + h)
|
||||
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
|
||||
if center_marks:
|
||||
mid_x = w / 2
|
||||
mid_y = h / 2
|
||||
mid_x = ox + w / 2
|
||||
mid_y = oy + h / 2
|
||||
ml = mark_length * 0.7
|
||||
# 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
|
||||
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
|
||||
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
|
||||
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.save()
|
||||
packet.seek(0)
|
||||
overlay = PdfReader(packet)
|
||||
page.merge_page(overlay.pages[0])
|
||||
overlay_reader = PdfReader(packet)
|
||||
overlay_page = overlay_reader.pages[0]
|
||||
|
||||
# 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:
|
||||
|
||||
39
app/engine/disassemble.py
Normal file
39
app/engine/disassemble.py
Normal 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
|
||||
@@ -10,7 +10,8 @@ def apply_masking(input_path, output_path, rectangles, apply_to="all"):
|
||||
"""Appliquer des rectangles blancs de masquage.
|
||||
|
||||
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)
|
||||
writer = PdfWriter()
|
||||
@@ -27,6 +28,8 @@ def apply_masking(input_path, output_path, rectangles, apply_to="all"):
|
||||
should_apply = page_num % 2 != 0
|
||||
elif isinstance(apply_to, int):
|
||||
should_apply = page_num == apply_to
|
||||
elif isinstance(apply_to, list):
|
||||
should_apply = i in apply_to
|
||||
|
||||
if should_apply and rectangles:
|
||||
w, h = get_page_size(page)
|
||||
|
||||
@@ -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.add_content import add_text_to_pages, add_image_to_pages
|
||||
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.extract_content import extract_text_from_page, extract_images_from_page
|
||||
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
||||
@@ -63,6 +64,7 @@ class MainWindow(QMainWindow):
|
||||
self._result_file = None
|
||||
self._original_file = None
|
||||
self._crop_settings = {}
|
||||
self._bleed_settings = None
|
||||
self._undo_stack = [] # pile de fichiers pour annuler
|
||||
self._redo_stack = []
|
||||
|
||||
@@ -221,11 +223,12 @@ class MainWindow(QMainWindow):
|
||||
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
|
||||
impose_menu.addSeparator()
|
||||
self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet)
|
||||
self._add_action(impose_menu, "Demonter les pages", None, self._do_disassemble)
|
||||
|
||||
# Pages
|
||||
pages_menu = menubar.addMenu("Pages")
|
||||
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()
|
||||
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, "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
|
||||
annot_menu = menubar.addMenu("Annotations")
|
||||
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.addAction("Livret", self._do_booklet)
|
||||
rtb.addAction("Delivretiser", self._do_unbooklet)
|
||||
rtb.addAction("Demonter", self._do_disassemble)
|
||||
rtb.addAction("N-Up", self._do_nup)
|
||||
rtb.addAction("Repetition", self._do_step_repeat)
|
||||
rtb.addAction("Manuel", self._do_manual_impose)
|
||||
@@ -328,7 +339,7 @@ class MainWindow(QMainWindow):
|
||||
# Section Pages
|
||||
rtb.addWidget(self._make_section_label("PAGES"))
|
||||
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("Inverser", self._do_reverse_pages)
|
||||
rtb.addAction("Fusionner", self._do_merge)
|
||||
@@ -633,6 +644,22 @@ class MainWindow(QMainWindow):
|
||||
self._hide_progress()
|
||||
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):
|
||||
if not self._require_file():
|
||||
return
|
||||
@@ -768,18 +795,51 @@ class MainWindow(QMainWindow):
|
||||
def _do_masking(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
dlg = MaskingDialog(self)
|
||||
if dlg.exec():
|
||||
from pypdf import PdfReader
|
||||
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()
|
||||
settings = dlg.get_settings()
|
||||
output = self._make_temp()
|
||||
try:
|
||||
apply_masking(self._current_file, output, **settings)
|
||||
self._show_result(output)
|
||||
self._status_label.setText(
|
||||
f"Masquage applique — {len(settings['rectangles'])} zone(s)"
|
||||
)
|
||||
except Exception as e:
|
||||
self._hide_progress()
|
||||
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):
|
||||
if not self._require_file():
|
||||
return
|
||||
@@ -791,6 +851,18 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
define_bleeds(self._current_file, output, **settings)
|
||||
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:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
@@ -804,15 +876,20 @@ class MainWindow(QMainWindow):
|
||||
reader = PdfReader(self._current_file)
|
||||
writer = PdfWriter()
|
||||
for page in reader.pages:
|
||||
if hasattr(page, 'trimbox'):
|
||||
page.trimbox = page.mediabox
|
||||
if hasattr(page, 'bleedbox'):
|
||||
page.bleedbox = page.mediabox
|
||||
# Remettre trimbox et bleedbox a la taille de la mediabox
|
||||
page.trimbox.lower_left = page.mediabox.lower_left
|
||||
page.trimbox.upper_right = page.mediabox.upper_right
|
||||
page.bleedbox.lower_left = page.mediabox.lower_left
|
||||
page.bleedbox.upper_right = page.mediabox.upper_right
|
||||
writer.add_page(page)
|
||||
output = self._make_temp()
|
||||
with open(output, "wb") as f:
|
||||
writer.write(f)
|
||||
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:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
@@ -976,20 +1053,21 @@ class MainWindow(QMainWindow):
|
||||
"crop_right": settings["crop_right"],
|
||||
}
|
||||
|
||||
# Appliquer le crop depuis l'original
|
||||
# Appliquer TOUS les crops cumules depuis l'original
|
||||
self._show_progress()
|
||||
source_file = self._original_file or self._current_file
|
||||
output = self._make_temp()
|
||||
try:
|
||||
crop_pages(source_file, output, **settings)
|
||||
crop_pages(source_file, output, per_page_settings=self._crop_settings)
|
||||
self._result_file = output
|
||||
self._current_file = output
|
||||
self._viewer.load_pdf(output)
|
||||
self._viewer.go_to_page(cur_page)
|
||||
self._page_panel.load_thumbnails(self._viewer)
|
||||
self._hide_progress()
|
||||
total_cropped = len(self._crop_settings)
|
||||
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)
|
||||
except Exception as e:
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
import pypdfium2 as pdfium
|
||||
|
||||
HANDLE_SIZE = 8 # taille des poignees de redimensionnement
|
||||
RULER_SIZE = 24 # largeur/hauteur des regles en pixels
|
||||
|
||||
|
||||
class SelectableImageLabel(QLabel):
|
||||
@@ -231,8 +232,105 @@ class SelectableImageLabel(QLabel):
|
||||
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):
|
||||
"""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)
|
||||
selection_changed = pyqtSignal(float, float, float, float)
|
||||
@@ -247,6 +345,8 @@ class PdfViewer(QWidget):
|
||||
self._zoom = 1.0
|
||||
self._page_width_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()
|
||||
|
||||
def _setup_ui(self):
|
||||
@@ -254,6 +354,18 @@ class PdfViewer(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 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.setWidgetResizable(True)
|
||||
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
@@ -268,7 +380,41 @@ class PdfViewer(QWidget):
|
||||
self._scroll.setWidget(self._image_label)
|
||||
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
||||
delta = event.angleDelta().y()
|
||||
@@ -324,11 +489,43 @@ class PdfViewer(QWidget):
|
||||
pil_image = bitmap.to_pil()
|
||||
data = pil_image.tobytes("raw", "RGBA")
|
||||
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)
|
||||
self._image_label.setPixmap(pixmap)
|
||||
self._image_label.adjustSize()
|
||||
self.page_changed.emit(self._current_page, self._total_pages)
|
||||
|
||||
if self._rulers_visible:
|
||||
self._update_rulers()
|
||||
|
||||
def _zoom_fit(self):
|
||||
if not self._pdf or self._current_page >= self._total_pages:
|
||||
return
|
||||
|
||||
@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
|
||||
BRANCH = "master"
|
||||
|
||||
# Version actuelle (incrementee a chaque release)
|
||||
VERSION = "3.5.0"
|
||||
VERSION = "3.6.0"
|
||||
|
||||
|
||||
def get_remote_version():
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.5.0
|
||||
3.6.0
|
||||
|
||||
Reference in New Issue
Block a user