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

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