- 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>
606 lines
22 KiB
Python
606 lines
22 KiB
Python
"""Widget de visualisation PDF avec pypdfium2 — V3 avec selection + regles."""
|
|
|
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QScrollArea, QSizePolicy, QApplication)
|
|
from PyQt6.QtGui import (QImage, QPixmap, QWheelEvent, QPainter, QPen, QColor,
|
|
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):
|
|
"""QLabel avec rectangle de selection redimensionnable par les angles."""
|
|
|
|
selection_changed = pyqtSignal(float, float, float, float)
|
|
selection_done = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._select_mode = False
|
|
self._has_selection = False
|
|
self._rect = QRect()
|
|
self._drag_mode = None # None, "create", "move", "tl", "tr", "bl", "br", "t", "b", "l", "r"
|
|
self._drag_start = QPoint()
|
|
self._drag_rect_start = QRect()
|
|
|
|
def set_select_mode(self, enabled):
|
|
self._select_mode = enabled
|
|
if enabled:
|
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
else:
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
def clear_selection(self):
|
|
self._has_selection = False
|
|
self._rect = QRect()
|
|
self.update()
|
|
|
|
def set_selection_from_margins(self, left_ratio, top_ratio, right_ratio, bottom_ratio):
|
|
if not self.pixmap():
|
|
return
|
|
pm = self.pixmap()
|
|
img_w, img_h = pm.width(), pm.height()
|
|
off_x, off_y = self._img_offset()
|
|
x1 = off_x + int(left_ratio * img_w)
|
|
y1 = off_y + int(top_ratio * img_h)
|
|
x2 = off_x + int(right_ratio * img_w)
|
|
y2 = off_y + int(bottom_ratio * img_h)
|
|
self._rect = QRect(QPoint(x1, y1), QPoint(x2, y2)).normalized()
|
|
self._has_selection = True
|
|
self.update()
|
|
|
|
def _img_offset(self):
|
|
if not self.pixmap():
|
|
return 0, 0
|
|
pm = self.pixmap()
|
|
return max(0, (self.width() - pm.width()) // 2), max(0, (self.height() - pm.height()) // 2)
|
|
|
|
def _handle_rects(self):
|
|
"""Retourne les rectangles des 8 poignees."""
|
|
if not self._has_selection:
|
|
return {}
|
|
r = self._rect.normalized()
|
|
hs = HANDLE_SIZE
|
|
return {
|
|
"tl": QRect(r.left() - hs, r.top() - hs, hs * 2, hs * 2),
|
|
"tr": QRect(r.right() - hs, r.top() - hs, hs * 2, hs * 2),
|
|
"bl": QRect(r.left() - hs, r.bottom() - hs, hs * 2, hs * 2),
|
|
"br": QRect(r.right() - hs, r.bottom() - hs, hs * 2, hs * 2),
|
|
"t": QRect(r.center().x() - hs, r.top() - hs, hs * 2, hs * 2),
|
|
"b": QRect(r.center().x() - hs, r.bottom() - hs, hs * 2, hs * 2),
|
|
"l": QRect(r.left() - hs, r.center().y() - hs, hs * 2, hs * 2),
|
|
"r": QRect(r.right() - hs, r.center().y() - hs, hs * 2, hs * 2),
|
|
}
|
|
|
|
def _cursor_for_handle(self, handle):
|
|
cursors = {
|
|
"tl": Qt.CursorShape.SizeFDiagCursor,
|
|
"br": Qt.CursorShape.SizeFDiagCursor,
|
|
"tr": Qt.CursorShape.SizeBDiagCursor,
|
|
"bl": Qt.CursorShape.SizeBDiagCursor,
|
|
"t": Qt.CursorShape.SizeVerCursor,
|
|
"b": Qt.CursorShape.SizeVerCursor,
|
|
"l": Qt.CursorShape.SizeHorCursor,
|
|
"r": Qt.CursorShape.SizeHorCursor,
|
|
}
|
|
return cursors.get(handle, Qt.CursorShape.CrossCursor)
|
|
|
|
def mousePressEvent(self, event):
|
|
if not self._select_mode or event.button() != Qt.MouseButton.LeftButton:
|
|
return super().mousePressEvent(event)
|
|
|
|
pos = event.pos()
|
|
self._drag_start = pos
|
|
self._drag_rect_start = QRect(self._rect)
|
|
|
|
# Verifier si on clique sur une poignee
|
|
if self._has_selection:
|
|
for name, hr in self._handle_rects().items():
|
|
if hr.contains(pos):
|
|
self._drag_mode = name
|
|
return
|
|
# Clic dans le rectangle = deplacer
|
|
if self._rect.normalized().contains(pos):
|
|
self._drag_mode = "move"
|
|
return
|
|
|
|
# Nouveau rectangle
|
|
self._drag_mode = "create"
|
|
self._rect = QRect(pos, pos)
|
|
self._has_selection = True
|
|
self.update()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
pos = event.pos()
|
|
|
|
if not self._select_mode:
|
|
return super().mouseMoveEvent(event)
|
|
|
|
# Hover : changer le curseur sur les poignees
|
|
if not self._drag_mode and self._has_selection:
|
|
for name, hr in self._handle_rects().items():
|
|
if hr.contains(pos):
|
|
self.setCursor(self._cursor_for_handle(name))
|
|
return
|
|
if self._rect.normalized().contains(pos):
|
|
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
|
return
|
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
return
|
|
|
|
if not self._drag_mode:
|
|
return
|
|
|
|
dx = pos.x() - self._drag_start.x()
|
|
dy = pos.y() - self._drag_start.y()
|
|
r = QRect(self._drag_rect_start)
|
|
|
|
if self._drag_mode == "create":
|
|
self._rect = QRect(self._drag_start, pos).normalized()
|
|
elif self._drag_mode == "move":
|
|
self._rect = r.translated(dx, dy)
|
|
elif self._drag_mode == "tl":
|
|
self._rect = QRect(QPoint(r.left() + dx, r.top() + dy), r.bottomRight()).normalized()
|
|
elif self._drag_mode == "tr":
|
|
self._rect = QRect(QPoint(r.left(), r.top() + dy), QPoint(r.right() + dx, r.bottom())).normalized()
|
|
elif self._drag_mode == "bl":
|
|
self._rect = QRect(QPoint(r.left() + dx, r.top()), QPoint(r.right(), r.bottom() + dy)).normalized()
|
|
elif self._drag_mode == "br":
|
|
self._rect = QRect(r.topLeft(), QPoint(r.right() + dx, r.bottom() + dy)).normalized()
|
|
elif self._drag_mode == "t":
|
|
self._rect = QRect(QPoint(r.left(), r.top() + dy), r.bottomRight()).normalized()
|
|
elif self._drag_mode == "b":
|
|
self._rect = QRect(r.topLeft(), QPoint(r.right(), r.bottom() + dy)).normalized()
|
|
elif self._drag_mode == "l":
|
|
self._rect = QRect(QPoint(r.left() + dx, r.top()), r.bottomRight()).normalized()
|
|
elif self._drag_mode == "r":
|
|
self._rect = QRect(r.topLeft(), QPoint(r.right() + dx, r.bottom())).normalized()
|
|
|
|
self.update()
|
|
self._emit_ratios()
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self._drag_mode:
|
|
self._drag_mode = None
|
|
self._emit_ratios()
|
|
self.selection_done.emit()
|
|
self.update()
|
|
else:
|
|
super().mouseReleaseEvent(event)
|
|
|
|
def _emit_ratios(self):
|
|
if not self.pixmap() or not self._has_selection:
|
|
return
|
|
pm = self.pixmap()
|
|
img_w, img_h = pm.width(), pm.height()
|
|
if img_w <= 0 or img_h <= 0:
|
|
return
|
|
off_x, off_y = self._img_offset()
|
|
r = self._rect.normalized()
|
|
|
|
left = max(0, min(1, (r.left() - off_x) / img_w))
|
|
top = max(0, min(1, (r.top() - off_y) / img_h))
|
|
right = max(0, min(1, (r.right() - off_x) / img_w))
|
|
bottom = max(0, min(1, (r.bottom() - off_y) / img_h))
|
|
|
|
self.selection_changed.emit(left, top, right, bottom)
|
|
|
|
def paintEvent(self, event):
|
|
super().paintEvent(event)
|
|
if not self._has_selection or self._rect.isNull():
|
|
return
|
|
|
|
painter = QPainter(self)
|
|
r = self._rect.normalized()
|
|
full = self.rect()
|
|
|
|
# Zone sombre en dehors
|
|
painter.setBrush(QColor(0, 0, 0, 80))
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
|
painter.drawRect(full.left(), full.top(), full.width(), r.top() - full.top())
|
|
painter.drawRect(full.left(), r.bottom(), full.width(), full.bottom() - r.bottom())
|
|
painter.drawRect(full.left(), r.top(), r.left() - full.left(), r.height())
|
|
painter.drawRect(r.right(), r.top(), full.right() - r.right(), r.height())
|
|
|
|
# Cadre
|
|
pen = QPen(QColor(0, 180, 216), 2, Qt.PenStyle.SolidLine)
|
|
painter.setPen(pen)
|
|
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
painter.drawRect(r)
|
|
|
|
# Poignees
|
|
painter.setBrush(QColor(255, 255, 255))
|
|
painter.setPen(QPen(QColor(0, 180, 216), 1))
|
|
for hr in self._handle_rects().values():
|
|
painter.drawRect(hr)
|
|
|
|
# Dimensions
|
|
if self.pixmap():
|
|
pm = self.pixmap()
|
|
img_w, img_h = pm.width(), pm.height()
|
|
off_x, off_y = self._img_offset()
|
|
if img_w > 0 and img_h > 0:
|
|
w_pct = r.width() / img_w * 100
|
|
h_pct = r.height() / img_h * 100
|
|
painter.setPen(QColor(0, 180, 216))
|
|
painter.drawText(r.left() + 4, r.top() - 6,
|
|
f"{w_pct:.0f}% x {h_pct:.0f}%")
|
|
|
|
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, selection et regles."""
|
|
|
|
page_changed = pyqtSignal(int, int)
|
|
selection_changed = pyqtSignal(float, float, float, float)
|
|
selection_done = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._pdf = None
|
|
self._pdf_path = None
|
|
self._current_page = 0
|
|
self._total_pages = 0
|
|
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):
|
|
layout = QVBoxLayout(self)
|
|
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)
|
|
self._scroll.setStyleSheet("QScrollArea { background: #d0d0d0; border: none; }")
|
|
|
|
self._image_label = SelectableImageLabel()
|
|
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
self._image_label.setStyleSheet("background: #d0d0d0;")
|
|
self._image_label.selection_changed.connect(self.selection_changed.emit)
|
|
self._image_label.selection_done.connect(self.selection_done.emit)
|
|
self._scroll.setWidget(self._image_label)
|
|
|
|
self._scroll.wheelEvent = self._on_wheel
|
|
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)
|
|
|
|
def clear_selection(self):
|
|
self._image_label.clear_selection()
|
|
|
|
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()
|
|
if delta > 0:
|
|
self._zoom = min(self._zoom * 1.15, 5.0)
|
|
else:
|
|
self._zoom = max(self._zoom / 1.15, 0.2)
|
|
self._render_page()
|
|
else:
|
|
if event.angleDelta().y() < 0:
|
|
self.next_page()
|
|
elif event.angleDelta().y() > 0:
|
|
self.prev_page()
|
|
|
|
def load_pdf(self, path):
|
|
try:
|
|
self._pdf_path = path
|
|
self._pdf = pdfium.PdfDocument(path)
|
|
self._total_pages = len(self._pdf)
|
|
self._current_page = 0
|
|
self._zoom_fit()
|
|
self._render_page()
|
|
except Exception as e:
|
|
self._image_label.setText(f"Erreur: {e}")
|
|
|
|
def load_pdf_bytes(self, data):
|
|
try:
|
|
self._pdf = pdfium.PdfDocument(data)
|
|
self._total_pages = len(self._pdf)
|
|
self._current_page = 0
|
|
self._zoom_fit()
|
|
self._render_page()
|
|
except Exception as e:
|
|
self._image_label.setText(f"Erreur: {e}")
|
|
|
|
def _render_page(self):
|
|
if not self._pdf or self._current_page >= self._total_pages:
|
|
return
|
|
page = self._pdf[self._current_page]
|
|
self._page_width_pt, self._page_height_pt = page.get_size()
|
|
scale = 150 / 72 * self._zoom
|
|
bitmap = page.render(scale=scale)
|
|
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
|
|
page = self._pdf[self._current_page]
|
|
pw, ph = page.get_size()
|
|
vw = self._scroll.viewport().width() - 20
|
|
vh = self._scroll.viewport().height() - 20
|
|
if vw <= 0 or vh <= 0:
|
|
self._zoom = 1.0
|
|
return
|
|
self._zoom = min(vw / pw, vh / ph) * (72 / 150)
|
|
|
|
def prev_page(self):
|
|
if self._current_page > 0:
|
|
self._current_page -= 1
|
|
self._render_page()
|
|
|
|
def next_page(self):
|
|
if self._current_page < self._total_pages - 1:
|
|
self._current_page += 1
|
|
self._render_page()
|
|
|
|
def go_to_page(self, page_index):
|
|
if 0 <= page_index < self._total_pages and page_index != self._current_page:
|
|
self._current_page = page_index
|
|
self._render_page()
|
|
|
|
def zoom_in(self):
|
|
self._zoom = min(self._zoom * 1.25, 5.0)
|
|
self._render_page()
|
|
|
|
def zoom_out(self):
|
|
self._zoom = max(self._zoom / 1.25, 0.2)
|
|
self._render_page()
|
|
|
|
def zoom_fit(self):
|
|
self._zoom_fit()
|
|
self._render_page()
|
|
|
|
def refresh(self):
|
|
self._render_page()
|
|
|
|
def get_page_thumbnail(self, page_index, width=120):
|
|
if not self._pdf or page_index >= self._total_pages:
|
|
return None
|
|
page = self._pdf[page_index]
|
|
pw, ph = page.get_size()
|
|
scale = width / pw
|
|
bitmap = page.render(scale=scale)
|
|
pil_image = bitmap.to_pil()
|
|
data = pil_image.tobytes("raw", "RGBA")
|
|
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
|
|
return QPixmap.fromImage(qimage)
|
|
|
|
@property
|
|
def current_page(self):
|
|
return self._current_page
|
|
|
|
@property
|
|
def total_pages(self):
|
|
return self._total_pages
|
|
|
|
@property
|
|
def zoom_level(self):
|
|
return self._zoom
|
|
|
|
@property
|
|
def pdf_path(self):
|
|
return self._pdf_path
|
|
|
|
@property
|
|
def page_width_pt(self):
|
|
return self._page_width_pt
|
|
|
|
@property
|
|
def page_height_pt(self):
|
|
return self._page_height_pt
|