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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user