Initial commit — Copydev Imposing Tool

Outil d'imposition PDF desktop (PyQt6) remplacant Quite Imposing Plus.
9 fonctions : livret, n-up, step & repeat, imposition manuelle,
jointure pages, pages blanches, numerotation, masquage, fonds perdus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-20 10:54:02 +01:00
commit e3dfd90933
30 changed files with 2364 additions and 0 deletions

0
app/__init__.py Normal file
View File

0
app/dialogs/__init__.py Normal file
View File

View File

@@ -0,0 +1,53 @@
"""Dialogue Definition des fonds perdus."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QDoubleSpinBox, QDialogButtonBox)
class BleedsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Fonds perdus (Bleeds)")
self.setMinimumWidth(350)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.addWidget(QLabel(
"Definir la zone de fond perdu autour de chaque page.\n"
"Cette zone sera prise en compte par les fonctions N-Up et Repetition."
))
grp = QGroupBox("Fonds perdus (mm)")
gl = QVBoxLayout(grp)
self.spins = {}
for label in ["Haut", "Bas", "Gauche", "Droite"]:
hl = QHBoxLayout()
hl.addWidget(QLabel(f"{label} :"))
spin = QDoubleSpinBox()
spin.setRange(0, 50)
spin.setValue(3)
spin.setSuffix(" mm")
hl.addWidget(spin)
hl.addStretch()
gl.addLayout(hl)
self.spins[label.lower()] = spin
layout.addWidget(grp)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
return {
"bleed_top": self.spins["haut"].value(),
"bleed_bottom": self.spins["bas"].value(),
"bleed_left": self.spins["gauche"].value(),
"bleed_right": self.spins["droite"].value(),
}

View File

@@ -0,0 +1,104 @@
"""Dialogue Creer un livret."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QRadioButton, QSpinBox,
QDialogButtonBox, QDoubleSpinBox)
from app.engine.pdf_utils import PAGE_SIZES
class BookletDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Creer un livret")
self.setMinimumWidth(400)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Type de reliure
grp_binding = QGroupBox("Type de reliure")
bl = QVBoxLayout(grp_binding)
self.rb_saddle = QRadioButton("Piqure a cheval (livret agrafe)")
self.rb_saddle.setChecked(True)
self.rb_perfect = QRadioButton("Dos carre colle (livre)")
self.rb_continuous = QRadioButton("Continu (pages sequentielles)")
self.rb_cut_stacks = QRadioButton("Cut stacks (couper et empiler)")
bl.addWidget(self.rb_saddle)
bl.addWidget(self.rb_perfect)
bl.addWidget(self.rb_continuous)
bl.addWidget(self.rb_cut_stacks)
layout.addWidget(grp_binding)
# Taille signature (perfect bound)
sig_layout = QHBoxLayout()
sig_layout.addWidget(QLabel("Pages par cahier :"))
self.spin_signature = QSpinBox()
self.spin_signature.setRange(4, 128)
self.spin_signature.setValue(32)
self.spin_signature.setSingleStep(4)
self.spin_signature.setEnabled(False)
sig_layout.addWidget(self.spin_signature)
sig_layout.addStretch()
layout.addLayout(sig_layout)
self.rb_perfect.toggled.connect(self.spin_signature.setEnabled)
# Taille de feuille
grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox()
self.combo_sheet.addItem("Automatique (2x page)")
for name in PAGE_SIZES:
self.combo_sheet.addItem(name)
sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet)
# Alignement
grp_align = QGroupBox("Alignement")
al = QVBoxLayout(grp_align)
self.rb_center = QRadioButton("Centrer")
self.rb_center.setChecked(True)
self.rb_pull = QRadioButton("Vers le dos (centre)")
self.rb_bl = QRadioButton("Bas-gauche")
al.addWidget(self.rb_center)
al.addWidget(self.rb_pull)
al.addWidget(self.rb_bl)
layout.addWidget(grp_align)
# Boutons OK/Annuler
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
"""Retourner les parametres selectionnes."""
if self.rb_saddle.isChecked():
binding = "saddle_stitch"
elif self.rb_perfect.isChecked():
binding = "perfect_bound"
elif self.rb_continuous.isChecked():
binding = "continuous"
else:
binding = "cut_stacks"
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES.get(sheet_text, None)
if self.rb_center.isChecked():
alignment = "center"
elif self.rb_pull.isChecked():
alignment = "pull_center"
else:
alignment = "bottom_left"
return {
"binding": binding,
"sheet_size": sheet_size,
"signature_size": self.spin_signature.value(),
"alignment": alignment,
}

View File

@@ -0,0 +1,82 @@
"""Dialogue Inserer des pages blanches."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QRadioButton, QSpinBox,
QDialogButtonBox)
class InsertBlankDialog(QDialog):
def __init__(self, total_pages=1, parent=None):
super().__init__(parent)
self.setWindowTitle("Inserer des pages blanches")
self.setMinimumWidth(400)
self._total_pages = total_pages
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
grp = QGroupBox("Mode d'insertion")
gl = QVBoxLayout(grp)
self.rb_after = QRadioButton("Apres la page :")
self.rb_after.setChecked(True)
after_layout = QHBoxLayout()
after_layout.addWidget(self.rb_after)
self.spin_pos = QSpinBox()
self.spin_pos.setRange(1, self._total_pages)
self.spin_pos.setValue(1)
after_layout.addWidget(self.spin_pos)
after_layout.addStretch()
gl.addLayout(after_layout)
self.rb_before = QRadioButton("Avant la page selectionnee")
gl.addWidget(self.rb_before)
every_layout = QHBoxLayout()
self.rb_every = QRadioButton("Toutes les")
every_layout.addWidget(self.rb_every)
self.spin_every = QSpinBox()
self.spin_every.setRange(1, 100)
self.spin_every.setValue(2)
self.spin_every.setEnabled(False)
every_layout.addWidget(self.spin_every)
every_layout.addWidget(QLabel("pages"))
every_layout.addStretch()
gl.addLayout(every_layout)
pad_layout = QHBoxLayout()
self.rb_pad = QRadioButton("Completer au multiple de")
pad_layout.addWidget(self.rb_pad)
self.spin_multiple = QSpinBox()
self.spin_multiple.setRange(2, 32)
self.spin_multiple.setValue(4)
self.spin_multiple.setEnabled(False)
pad_layout.addWidget(self.spin_multiple)
pad_layout.addWidget(QLabel("pages"))
pad_layout.addStretch()
gl.addLayout(pad_layout)
layout.addWidget(grp)
# Activer/desactiver les spinbox
self.rb_every.toggled.connect(self.spin_every.setEnabled)
self.rb_pad.toggled.connect(self.spin_multiple.setEnabled)
# Boutons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
if self.rb_after.isChecked():
return {"mode": "after", "position": self.spin_pos.value()}
elif self.rb_before.isChecked():
return {"mode": "before", "position": self.spin_pos.value()}
elif self.rb_every.isChecked():
return {"mode": "every_n", "every_n": self.spin_every.value()}
else:
return {"mode": "pad_to_multiple", "target_multiple": self.spin_multiple.value()}

View File

@@ -0,0 +1,38 @@
"""Dialogue Joindre deux pages."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QLabel, QGroupBox,
QRadioButton, QDialogButtonBox)
class JoinPagesDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Joindre deux pages")
self.setMinimumWidth(300)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Les pages seront jointes par paires (1+2, 3+4, ...)."))
grp = QGroupBox("Direction")
gl = QVBoxLayout(grp)
self.rb_horizontal = QRadioButton("Cote a cote (horizontal)")
self.rb_horizontal.setChecked(True)
self.rb_vertical = QRadioButton("Dessus/dessous (vertical)")
gl.addWidget(self.rb_horizontal)
gl.addWidget(self.rb_vertical)
layout.addWidget(grp)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
return {
"direction": "horizontal" if self.rb_horizontal.isChecked() else "vertical"
}

View File

@@ -0,0 +1,122 @@
"""Dialogue Imposition manuelle."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox,
QDialogButtonBox, QPushButton, QListWidget,
QCheckBox)
from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt
class ManualImposeDialog(QDialog):
def __init__(self, total_pages=1, parent=None):
super().__init__(parent)
self.setWindowTitle("Imposition manuelle")
self.setMinimumWidth(500)
self._total_pages = total_pages
self._placements = []
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille cible")
sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox()
for name in PAGE_SIZES:
self.combo_sheet.addItem(name)
self.combo_sheet.setCurrentText("A3 Paysage")
sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet)
# Placement
grp_place = QGroupBox("Placer une page")
pl = QHBoxLayout(grp_place)
pl.addWidget(QLabel("Page:"))
self.spin_page = QSpinBox()
self.spin_page.setRange(1, self._total_pages)
pl.addWidget(self.spin_page)
pl.addWidget(QLabel("X:"))
self.spin_x = QDoubleSpinBox()
self.spin_x.setRange(0, 2000)
self.spin_x.setSuffix(" mm")
pl.addWidget(self.spin_x)
pl.addWidget(QLabel("Y:"))
self.spin_y = QDoubleSpinBox()
self.spin_y.setRange(0, 2000)
self.spin_y.setSuffix(" mm")
pl.addWidget(self.spin_y)
pl.addWidget(QLabel("Rot:"))
self.combo_rot = QComboBox()
self.combo_rot.addItems(["0", "90", "180", "270"])
pl.addWidget(self.combo_rot)
pl.addWidget(QLabel("Ech:"))
self.spin_scale = QDoubleSpinBox()
self.spin_scale.setRange(0.1, 10.0)
self.spin_scale.setValue(1.0)
self.spin_scale.setSingleStep(0.1)
pl.addWidget(self.spin_scale)
layout.addWidget(grp_place)
# Boutons
btn_layout = QHBoxLayout()
btn_add = QPushButton("Ajouter")
btn_add.clicked.connect(self._add_placement)
btn_layout.addWidget(btn_add)
btn_del = QPushButton("Supprimer")
btn_del.clicked.connect(self._del_placement)
btn_layout.addWidget(btn_del)
btn_layout.addStretch()
layout.addLayout(btn_layout)
# Liste
self.list_placements = QListWidget()
layout.addWidget(self.list_placements)
# Crop marks
self.chk_crop = QCheckBox("Reperes de coupe")
layout.addWidget(self.chk_crop)
# Boutons OK/Annuler
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _add_placement(self):
p = {
"page": self.spin_page.value(),
"x": mm_to_pt(self.spin_x.value()),
"y": mm_to_pt(self.spin_y.value()),
"rotation": int(self.combo_rot.currentText()),
"scale": self.spin_scale.value(),
}
self._placements.append(p)
self.list_placements.addItem(
f"Page {p['page']} - X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} mm "
f"Rot={self.combo_rot.currentText()} Ech={self.spin_scale.value():.1f}"
)
def _del_placement(self):
row = self.list_placements.currentRow()
if row >= 0:
self.list_placements.takeItem(row)
self._placements.pop(row)
def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return {
"sheet_size": sheet_size,
"placements": self._placements,
"crop_marks": self.chk_crop.isChecked(),
}

View File

@@ -0,0 +1,112 @@
"""Dialogue Masquage (rectangles blancs)."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QDoubleSpinBox,
QDialogButtonBox, QPushButton, QListWidget,
QListWidgetItem)
from app.engine.pdf_utils import mm_to_pt
class MaskingDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Masquage (ruban blanc)")
self.setMinimumWidth(450)
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)
rl.addWidget(QLabel("X:"))
self.spin_x = QDoubleSpinBox()
self.spin_x.setRange(0, 1000)
self.spin_x.setSuffix(" mm")
rl.addWidget(self.spin_x)
rl.addWidget(QLabel("Y:"))
self.spin_y = QDoubleSpinBox()
self.spin_y.setRange(0, 1000)
self.spin_y.setSuffix(" mm")
rl.addWidget(self.spin_y)
rl.addWidget(QLabel("L:"))
self.spin_w = QDoubleSpinBox()
self.spin_w.setRange(1, 1000)
self.spin_w.setValue(50)
self.spin_w.setSuffix(" mm")
rl.addWidget(self.spin_w)
rl.addWidget(QLabel("H:"))
self.spin_h = QDoubleSpinBox()
self.spin_h.setRange(1, 1000)
self.spin_h.setValue(10)
self.spin_h.setSuffix(" mm")
rl.addWidget(self.spin_h)
layout.addWidget(grp_rect)
# Boutons ajouter/supprimer
btn_layout = QHBoxLayout()
btn_add = QPushButton("Ajouter")
btn_add.clicked.connect(self._add_rect)
btn_layout.addWidget(btn_add)
btn_del = QPushButton("Supprimer")
btn_del.clicked.connect(self._del_rect)
btn_layout.addWidget(btn_del)
btn_layout.addStretch()
layout.addLayout(btn_layout)
# Liste des rectangles
self.list_rects = QListWidget()
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)
layout.addWidget(grp_apply)
# Boutons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _add_rect(self):
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()),
}
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"
)
def _del_rect(self):
row = self.list_rects.currentRow()
if row >= 0:
self.list_rects.takeItem(row)
self._rectangles.pop(row)
def get_settings(self):
apply_map = {
"Toutes les pages": "all",
"Pages paires": "even",
"Pages impaires": "odd"
}
return {
"rectangles": self._rectangles,
"apply_to": apply_map[self.combo_apply.currentText()],
}

131
app/dialogs/nup_dialog.py Normal file
View File

@@ -0,0 +1,131 @@
"""Dialogue N pages par feuille."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QSpinBox, QCheckBox,
QDialogButtonBox, QDoubleSpinBox, QFileDialog)
from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt
class NupDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("N pages par feuille")
self.setMinimumWidth(450)
self._bg_path = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Grille
grp_grid = QGroupBox("Disposition")
gl = QHBoxLayout(grp_grid)
gl.addWidget(QLabel("Colonnes :"))
self.spin_cols = QSpinBox()
self.spin_cols.setRange(1, 10)
self.spin_cols.setValue(2)
gl.addWidget(self.spin_cols)
gl.addWidget(QLabel("Lignes :"))
self.spin_rows = QSpinBox()
self.spin_rows.setRange(1, 10)
self.spin_rows.setValue(2)
gl.addWidget(self.spin_rows)
layout.addWidget(grp_grid)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox()
for name in PAGE_SIZES:
self.combo_sheet.addItem(name)
self.combo_sheet.setCurrentText("A4 Paysage")
sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet)
# Marges
grp_margins = QGroupBox("Marges (mm)")
ml = QHBoxLayout(grp_margins)
self.margins = {}
for label in ["Haut", "Bas", "Gauche", "Droite"]:
ml.addWidget(QLabel(f"{label}:"))
spin = QDoubleSpinBox()
spin.setRange(0, 100)
spin.setValue(5)
spin.setSuffix(" mm")
ml.addWidget(spin)
self.margins[label.lower()] = spin
layout.addWidget(grp_margins)
# Gouttiere
grp_gutter = QGroupBox("Gouttiere (mm)")
gutl = QHBoxLayout(grp_gutter)
gutl.addWidget(QLabel("Horizontale :"))
self.spin_gutter_h = QDoubleSpinBox()
self.spin_gutter_h.setRange(0, 50)
self.spin_gutter_h.setValue(0)
self.spin_gutter_h.setSuffix(" mm")
gutl.addWidget(self.spin_gutter_h)
gutl.addWidget(QLabel("Verticale :"))
self.spin_gutter_v = QDoubleSpinBox()
self.spin_gutter_v.setRange(0, 50)
self.spin_gutter_v.setValue(0)
self.spin_gutter_v.setSuffix(" mm")
gutl.addWidget(self.spin_gutter_v)
layout.addWidget(grp_gutter)
# Options
grp_opts = QGroupBox("Options")
ol = QVBoxLayout(grp_opts)
self.chk_fit = QCheckBox("Ajuster les pages a la cellule")
self.chk_fit.setChecked(True)
ol.addWidget(self.chk_fit)
self.chk_crop = QCheckBox("Reperes de coupe")
ol.addWidget(self.chk_crop)
bg_layout = QHBoxLayout()
self.chk_bg = QCheckBox("Fond PDF :")
bg_layout.addWidget(self.chk_bg)
self.lbl_bg = QLabel("Aucun")
bg_layout.addWidget(self.lbl_bg)
from PyQt6.QtWidgets import QPushButton
btn_bg = QPushButton("Choisir...")
btn_bg.clicked.connect(self._choose_bg)
bg_layout.addWidget(btn_bg)
bg_layout.addStretch()
ol.addLayout(bg_layout)
layout.addWidget(grp_opts)
# Boutons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _choose_bg(self):
path, _ = QFileDialog.getOpenFileName(self, "Choisir un fond PDF", "", "PDF (*.pdf)")
if path:
self._bg_path = path
self.lbl_bg.setText(path.split("/")[-1].split("\\")[-1])
self.chk_bg.setChecked(True)
def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return {
"cols": self.spin_cols.value(),
"rows": self.spin_rows.value(),
"sheet_size": sheet_size,
"margin_top": mm_to_pt(self.margins["haut"].value()),
"margin_bottom": mm_to_pt(self.margins["bas"].value()),
"margin_left": mm_to_pt(self.margins["gauche"].value()),
"margin_right": mm_to_pt(self.margins["droite"].value()),
"gutter_h": mm_to_pt(self.spin_gutter_h.value()),
"gutter_v": mm_to_pt(self.spin_gutter_v.value()),
"scale_mode": "fit" if self.chk_fit.isChecked() else "none",
"crop_marks": self.chk_crop.isChecked(),
"background_path": self._bg_path if self.chk_bg.isChecked() else None,
}

View File

@@ -0,0 +1,120 @@
"""Dialogue Numerotation de pages."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QSpinBox, QLineEdit,
QDialogButtonBox, QDoubleSpinBox)
from reportlab.lib.units import mm
class PageNumbersDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Numerotation de pages")
self.setMinimumWidth(400)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Position
grp_pos = QGroupBox("Position")
pl = QHBoxLayout(grp_pos)
pl.addWidget(QLabel("Emplacement :"))
self.combo_pos = QComboBox()
positions = [
"haut-gauche", "haut-centre", "haut-droite",
"milieu-gauche", "milieu-centre", "milieu-droite",
"bas-gauche", "bas-centre", "bas-droite"
]
self.combo_pos.addItems(positions)
self.combo_pos.setCurrentText("bas-centre")
pl.addWidget(self.combo_pos)
layout.addWidget(grp_pos)
# Police
grp_font = QGroupBox("Police")
fl = QHBoxLayout(grp_font)
fl.addWidget(QLabel("Police :"))
self.combo_font = QComboBox()
self.combo_font.addItems(["Helvetica", "Times-Roman", "Courier"])
fl.addWidget(self.combo_font)
fl.addWidget(QLabel("Taille :"))
self.spin_size = QSpinBox()
self.spin_size.setRange(4, 72)
self.spin_size.setValue(10)
fl.addWidget(self.spin_size)
layout.addWidget(grp_font)
# Texte
grp_text = QGroupBox("Texte")
tl = QHBoxLayout(grp_text)
tl.addWidget(QLabel("Prefixe :"))
self.edit_prefix = QLineEdit()
self.edit_prefix.setPlaceholderText("ex: Page ")
tl.addWidget(self.edit_prefix)
tl.addWidget(QLabel("Suffixe :"))
self.edit_suffix = QLineEdit()
self.edit_suffix.setPlaceholderText("ex: / 24")
tl.addWidget(self.edit_suffix)
layout.addWidget(grp_text)
# Numero de depart
start_layout = QHBoxLayout()
start_layout.addWidget(QLabel("Commencer a :"))
self.spin_start = QSpinBox()
self.spin_start.setRange(0, 9999)
self.spin_start.setValue(1)
start_layout.addWidget(self.spin_start)
start_layout.addStretch()
layout.addLayout(start_layout)
# Marges
margin_layout = QHBoxLayout()
margin_layout.addWidget(QLabel("Marge X :"))
self.spin_mx = QDoubleSpinBox()
self.spin_mx.setRange(0, 100)
self.spin_mx.setValue(10)
self.spin_mx.setSuffix(" mm")
margin_layout.addWidget(self.spin_mx)
margin_layout.addWidget(QLabel("Marge Y :"))
self.spin_my = QDoubleSpinBox()
self.spin_my.setRange(0, 100)
self.spin_my.setValue(10)
self.spin_my.setSuffix(" mm")
margin_layout.addWidget(self.spin_my)
layout.addLayout(margin_layout)
# 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)
layout.addWidget(grp_apply)
# Boutons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
apply_map = {
"Toutes les pages": "all",
"Pages paires": "even",
"Pages impaires": "odd"
}
return {
"position": self.combo_pos.currentText(),
"font_name": self.combo_font.currentText(),
"font_size": self.spin_size.value(),
"color": (0, 0, 0),
"prefix": self.edit_prefix.text(),
"suffix": self.edit_suffix.text(),
"start_number": self.spin_start.value(),
"margin_x": self.spin_mx.value() * mm,
"margin_y": self.spin_my.value() * mm,
"apply_to": apply_map[self.combo_apply.currentText()],
}

View File

@@ -0,0 +1,95 @@
"""Dialogue Repetition (Step & Repeat)."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QSpinBox, QCheckBox,
QDialogButtonBox, QDoubleSpinBox)
from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt
class StepRepeatDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Repetition (Step & Repeat)")
self.setMinimumWidth(400)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox()
for name in PAGE_SIZES:
self.combo_sheet.addItem(name)
self.combo_sheet.setCurrentText("A3 Paysage")
sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet)
# Nombre de copies
grp_copies = QGroupBox("Nombre de copies")
cl = QVBoxLayout(grp_copies)
self.chk_auto = QCheckBox("Automatique (remplir la feuille)")
self.chk_auto.setChecked(True)
cl.addWidget(self.chk_auto)
manual = QHBoxLayout()
manual.addWidget(QLabel("Horizontal :"))
self.spin_h = QSpinBox()
self.spin_h.setRange(1, 20)
self.spin_h.setValue(2)
self.spin_h.setEnabled(False)
manual.addWidget(self.spin_h)
manual.addWidget(QLabel("Vertical :"))
self.spin_v = QSpinBox()
self.spin_v.setRange(1, 20)
self.spin_v.setValue(2)
self.spin_v.setEnabled(False)
manual.addWidget(self.spin_v)
cl.addLayout(manual)
layout.addWidget(grp_copies)
self.chk_auto.toggled.connect(lambda checked: self.spin_h.setEnabled(not checked))
self.chk_auto.toggled.connect(lambda checked: self.spin_v.setEnabled(not checked))
# Espacement
grp_spacing = QGroupBox("Espacement (mm)")
spl = QHBoxLayout(grp_spacing)
spl.addWidget(QLabel("Horizontal :"))
self.spin_sp_h = QDoubleSpinBox()
self.spin_sp_h.setRange(0, 50)
self.spin_sp_h.setValue(0)
self.spin_sp_h.setSuffix(" mm")
spl.addWidget(self.spin_sp_h)
spl.addWidget(QLabel("Vertical :"))
self.spin_sp_v = QDoubleSpinBox()
self.spin_sp_v.setRange(0, 50)
self.spin_sp_v.setValue(0)
self.spin_sp_v.setSuffix(" mm")
spl.addWidget(self.spin_sp_v)
layout.addWidget(grp_spacing)
# Options
self.chk_crop = QCheckBox("Reperes de coupe")
layout.addWidget(self.chk_crop)
# Boutons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return {
"sheet_size": sheet_size,
"copies_h": None if self.chk_auto.isChecked() else self.spin_h.value(),
"copies_v": None if self.chk_auto.isChecked() else self.spin_v.value(),
"spacing_h": mm_to_pt(self.spin_sp_h.value()),
"spacing_v": mm_to_pt(self.spin_sp_v.value()),
"crop_marks": self.chk_crop.isChecked(),
}

0
app/engine/__init__.py Normal file
View File

36
app/engine/bleeds.py Normal file
View File

@@ -0,0 +1,36 @@
"""Moteur Definition des fonds perdus (bleeds)."""
from pypdf import PdfReader, PdfWriter
from .pdf_utils import get_page_size, mm_to_pt
def define_bleeds(input_path, output_path, bleed_top=0, bleed_bottom=0,
bleed_left=0, bleed_right=0):
"""Definir les zones de fond perdu en etendant le TrimBox/BleedBox.
Les valeurs sont en millimetres.
"""
bt = mm_to_pt(bleed_top)
bb = mm_to_pt(bleed_bottom)
bl = mm_to_pt(bleed_left)
br = mm_to_pt(bleed_right)
reader = PdfReader(input_path)
writer = PdfWriter()
for page in reader.pages:
w, h = get_page_size(page)
# Le TrimBox definit la taille finale apres coupe
page.trimbox.lower_left = (bl, bb)
page.trimbox.upper_right = (w - br, h - bt)
# Le BleedBox inclut le fond perdu
page.bleedbox.lower_left = (0, 0)
page.bleedbox.upper_right = (w, h)
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

196
app/engine/booklet.py Normal file
View File

@@ -0,0 +1,196 @@
"""Moteur de creation de livrets."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size, create_blank_page, pad_to_multiple
import io
import math
def create_booklet(input_path, output_path, binding="saddle_stitch", sheet_size=None,
signature_size=32, alignment="center"):
"""Creer un livret a partir d'un PDF.
binding: "continuous", "saddle_stitch", "perfect_bound", "cut_stacks"
sheet_size: (largeur, hauteur) en points, ou None pour auto (2x page)
signature_size: nombre de pages par cahier (perfect_bound uniquement)
alignment: "bottom_left", "center", "pull_center"
"""
reader = PdfReader(input_path)
if binding == "continuous":
return _booklet_continuous(reader, output_path, sheet_size, alignment)
elif binding == "saddle_stitch":
return _booklet_saddle_stitch(reader, output_path, sheet_size, alignment)
elif binding == "perfect_bound":
return _booklet_perfect_bound(reader, output_path, sheet_size, signature_size, alignment)
elif binding == "cut_stacks":
return _booklet_cut_stacks(reader, output_path, sheet_size, alignment)
def _get_sheet_size(reader, sheet_size):
"""Determiner la taille de la feuille."""
page_w, page_h = get_page_size(reader.pages[0])
if sheet_size:
return sheet_size
# Auto: feuille = 2x largeur de page
return (page_w * 2, page_h)
def _place_two_pages(left_page, right_page, sheet_w, sheet_h, page_w, page_h, alignment):
"""Placer deux pages cote a cote sur une feuille."""
sheet = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
half_w = sheet_w / 2
if alignment == "center":
left_x = (half_w - page_w) / 2
right_x = half_w + (half_w - page_w) / 2
y = (sheet_h - page_h) / 2
elif alignment == "pull_center":
left_x = half_w - page_w
right_x = half_w
y = (sheet_h - page_h) / 2
else: # bottom_left
left_x = 0
right_x = half_w
y = 0
if left_page:
left_copy = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
left_copy.merge_page(left_page)
left_copy.add_transformation(Transformation().translate(left_x, y))
sheet.merge_page(left_copy)
if right_page:
right_copy = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
right_copy.merge_page(right_page)
right_copy.add_transformation(Transformation().translate(right_x, y))
sheet.merge_page(right_copy)
return sheet
def _booklet_continuous(reader, output_path, sheet_size, alignment):
"""Mode continu: pages 1+2, 3+4, etc."""
reader = pad_to_multiple(reader, 2)
pages = reader.pages
page_w, page_h = get_page_size(pages[0])
sheet_w, sheet_h = _get_sheet_size(reader, sheet_size)
writer = PdfWriter()
for i in range(0, len(pages), 2):
left = pages[i]
right = pages[i + 1] if i + 1 < len(pages) else None
sheet = _place_two_pages(left, right, sheet_w, sheet_h, page_w, page_h, alignment)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path
def _saddle_stitch_order(num_pages):
"""Calculer l'ordre des pages pour piqure a cheval.
Pour N pages (multiple de 4):
Feuille 1 recto: page N, page 1
Feuille 1 verso: page 2, page N-1
Feuille 2 recto: page N-2, page 3
etc.
"""
pairs = []
n = num_pages
for i in range(n // 4):
# Recto
pairs.append((n - 1 - 2 * i, 2 * i))
# Verso
pairs.append((2 * i + 1, n - 2 - 2 * i))
return pairs
def _booklet_saddle_stitch(reader, output_path, sheet_size, alignment):
"""Piqure a cheval."""
reader = pad_to_multiple(reader, 4)
pages = reader.pages
num_pages = len(pages)
page_w, page_h = get_page_size(pages[0])
sheet_w, sheet_h = _get_sheet_size(reader, sheet_size)
pairs = _saddle_stitch_order(num_pages)
writer = PdfWriter()
for left_idx, right_idx in pairs:
left = pages[left_idx] if left_idx < num_pages else None
right = pages[right_idx] if right_idx < num_pages else None
sheet = _place_two_pages(left, right, sheet_w, sheet_h, page_w, page_h, alignment)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path
def _booklet_perfect_bound(reader, output_path, sheet_size, signature_size, alignment):
"""Dos carre colle: plusieurs cahiers de signature_size pages."""
reader = pad_to_multiple(reader, 4)
pages = reader.pages
num_pages = len(pages)
page_w, page_h = get_page_size(pages[0])
sheet_w, sheet_h = _get_sheet_size(reader, sheet_size)
writer = PdfWriter()
# Decouper en signatures
for sig_start in range(0, num_pages, signature_size):
sig_end = min(sig_start + signature_size, num_pages)
sig_pages = list(range(sig_start, sig_end))
# Padding de la signature
while len(sig_pages) % 4 != 0:
sig_pages.append(None)
n = len(sig_pages)
for i in range(n // 4):
# Recto
left_idx = sig_pages[n - 1 - 2 * i]
right_idx = sig_pages[2 * i]
left = pages[left_idx] if left_idx is not None and left_idx < num_pages else None
right = pages[right_idx] if right_idx is not None and right_idx < num_pages else None
sheet = _place_two_pages(left, right, sheet_w, sheet_h, page_w, page_h, alignment)
writer.add_page(sheet)
# Verso
left_idx = sig_pages[2 * i + 1]
right_idx = sig_pages[n - 2 - 2 * i]
left = pages[left_idx] if left_idx is not None and left_idx < num_pages else None
right = pages[right_idx] if right_idx is not None and right_idx < num_pages else None
sheet = _place_two_pages(left, right, sheet_w, sheet_h, page_w, page_h, alignment)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path
def _booklet_cut_stacks(reader, output_path, sheet_size, alignment):
"""Cut stacks: imprimer, couper en deux, empiler."""
reader = pad_to_multiple(reader, 4)
pages = reader.pages
num_pages = len(pages)
page_w, page_h = get_page_size(pages[0])
sheet_w, sheet_h = _get_sheet_size(reader, sheet_size)
writer = PdfWriter()
half = num_pages // 2
for i in range(half):
top_idx = i
bottom_idx = half + i
left = pages[top_idx] if top_idx < num_pages else None
right = pages[bottom_idx] if bottom_idx < num_pages else None
sheet = _place_two_pages(left, right, sheet_w, sheet_h, page_w, page_h, alignment)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

View File

@@ -0,0 +1,58 @@
"""Moteur Insertion de pages blanches."""
from pypdf import PdfReader, PdfWriter
from .pdf_utils import get_page_size
def insert_blank_pages(input_path, output_path, mode="after", position=None,
every_n=None, target_multiple=None, page_size=None):
"""Inserer des pages blanches.
mode: "before", "after", "every_n", "pad_to_multiple"
position: numero de page (1-based) pour before/after
every_n: inserer apres chaque N pages
target_multiple: arrondir au multiple de N
page_size: (w, h) en points ou None pour meme taille que page adjacente
"""
reader = PdfReader(input_path)
writer = PdfWriter()
if mode == "pad_to_multiple" and target_multiple:
for page in reader.pages:
writer.add_page(page)
num = len(reader.pages)
remainder = num % target_multiple
if remainder != 0:
to_add = target_multiple - remainder
last_w, last_h = get_page_size(reader.pages[-1])
w, h = page_size if page_size else (last_w, last_h)
for _ in range(to_add):
writer.add_blank_page(w, h)
elif mode == "every_n" and every_n:
for i, page in enumerate(reader.pages):
writer.add_page(page)
if (i + 1) % every_n == 0 and i + 1 < len(reader.pages):
w, h = get_page_size(page)
bw, bh = page_size if page_size else (w, h)
writer.add_blank_page(bw, bh)
elif mode in ("before", "after") and position:
idx = position - 1 # 0-based
for i, page in enumerate(reader.pages):
if mode == "before" and i == idx:
w, h = get_page_size(page)
bw, bh = page_size if page_size else (w, h)
writer.add_blank_page(bw, bh)
writer.add_page(page)
if mode == "after" and i == idx:
w, h = get_page_size(page)
bw, bh = page_size if page_size else (w, h)
writer.add_blank_page(bw, bh)
else:
for page in reader.pages:
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

60
app/engine/join_pages.py Normal file
View File

@@ -0,0 +1,60 @@
"""Moteur Joindre deux pages."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size
def join_pages(input_path, output_path, direction="horizontal"):
"""Joindre les pages par paires.
direction: "horizontal" (cote a cote) ou "vertical" (dessus/dessous)
"""
reader = PdfReader(input_path)
pages = reader.pages
writer = PdfWriter()
for i in range(0, len(pages), 2):
page1 = pages[i]
page2 = pages[i + 1] if i + 1 < len(pages) else None
w1, h1 = get_page_size(page1)
if page2:
w2, h2 = get_page_size(page2)
else:
w2, h2 = w1, h1
if direction == "horizontal":
new_w = w1 + w2
new_h = max(h1, h2)
sheet = PageObject.create_blank_page(width=new_w, height=new_h)
overlay1 = PageObject.create_blank_page(width=new_w, height=new_h)
overlay1.merge_page(page1)
sheet.merge_page(overlay1)
if page2:
overlay2 = PageObject.create_blank_page(width=new_w, height=new_h)
overlay2.merge_page(page2)
overlay2.add_transformation(Transformation().translate(w1, 0))
sheet.merge_page(overlay2)
else: # vertical
new_w = max(w1, w2)
new_h = h1 + h2
sheet = PageObject.create_blank_page(width=new_w, height=new_h)
overlay1 = PageObject.create_blank_page(width=new_w, height=new_h)
overlay1.merge_page(page1)
overlay1.add_transformation(Transformation().translate(0, h2))
sheet.merge_page(overlay1)
if page2:
overlay2 = PageObject.create_blank_page(width=new_w, height=new_h)
overlay2.merge_page(page2)
sheet.merge_page(overlay2)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

View File

@@ -0,0 +1,68 @@
"""Moteur Imposition manuelle."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size, create_crop_marks_page
def manual_impose(input_path, output_path, sheet_size, placements, crop_marks=False):
"""Imposition manuelle : placer des pages a des positions precises.
sheet_size: (largeur, hauteur) en points
placements: liste de dicts:
{"page": int (1-based), "x": float, "y": float, "scale": float, "rotation": int}
"""
reader = PdfReader(input_path)
pages = reader.pages
sheet_w, sheet_h = sheet_size
writer = PdfWriter()
sheet = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
mark_positions = []
for p in placements:
page_idx = p["page"] - 1
if page_idx < 0 or page_idx >= len(pages):
continue
page = pages[page_idx]
page_w, page_h = get_page_size(page)
x = p.get("x", 0)
y = p.get("y", 0)
scale = p.get("scale", 1.0)
rotation = p.get("rotation", 0)
overlay = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
overlay.merge_page(page)
transform = Transformation()
if rotation:
# Rotation autour du centre de la page
cx = page_w / 2
cy = page_h / 2
transform = transform.translate(-cx, -cy)
transform = transform.rotate(rotation)
transform = transform.translate(cx, cy)
transform = transform.scale(scale, scale).translate(x, y)
overlay.add_transformation(transform)
sheet.merge_page(overlay)
if crop_marks:
scaled_w = page_w * scale
scaled_h = page_h * scale
mark_positions.extend([
(x, y),
(x + scaled_w, y),
(x, y + scaled_h),
(x + scaled_w, y + scaled_h),
])
if crop_marks and mark_positions:
marks = create_crop_marks_page(sheet_w, sheet_h, mark_positions)
sheet.merge_page(marks)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

53
app/engine/masking.py Normal file
View File

@@ -0,0 +1,53 @@
"""Moteur Masquage (Masking Tape) — rectangles blancs."""
from pypdf import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from .pdf_utils import get_page_size
import io
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)
"""
reader = PdfReader(input_path)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
page_num = i + 1
should_apply = False
if apply_to == "all":
should_apply = True
elif apply_to == "even":
should_apply = page_num % 2 == 0
elif apply_to == "odd":
should_apply = page_num % 2 != 0
elif isinstance(apply_to, int):
should_apply = page_num == apply_to
if should_apply and rectangles:
w, h = get_page_size(page)
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(w, h))
c.setFillColorRGB(1, 1, 1)
c.setStrokeColorRGB(1, 1, 1)
for rect in rectangles:
c.rect(rect["x"], rect["y"], rect["width"], rect["height"],
fill=1, stroke=0)
c.showPage()
c.save()
packet.seek(0)
overlay_reader = PdfReader(packet)
page.merge_page(overlay_reader.pages[0])
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

102
app/engine/nup.py Normal file
View File

@@ -0,0 +1,102 @@
"""Moteur N-Up : placer plusieurs pages par feuille."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size, create_blank_page, mm_to_pt, smart_crop_marks
import io
def nup_pages(input_path, output_path, cols=2, rows=2, sheet_size=(841.89, 595.28),
margin_top=0, margin_bottom=0, margin_left=0, margin_right=0,
gutter_h=0, gutter_v=0, scale_mode="fit", crop_marks=False,
background_path=None):
"""Placer plusieurs pages sur chaque feuille.
cols, rows: grille
sheet_size: (largeur, hauteur) en points
margins: en points
gutter_h, gutter_v: espacement horizontal/vertical entre pages
scale_mode: "fit" (ajuster) ou "none" (taille reelle)
crop_marks: ajouter des reperes de coupe
background_path: chemin vers un PDF de fond
"""
reader = PdfReader(input_path)
pages = reader.pages
sheet_w, sheet_h = sheet_size
# Zone utile
usable_w = sheet_w - margin_left - margin_right - gutter_h * (cols - 1)
usable_h = sheet_h - margin_top - margin_bottom - gutter_v * (rows - 1)
cell_w = usable_w / cols
cell_h = usable_h / rows
# Fond
bg_page = None
if background_path:
bg_reader = PdfReader(background_path)
if bg_reader.pages:
bg_page = bg_reader.pages[0]
writer = PdfWriter()
pages_per_sheet = cols * rows
for sheet_idx in range(0, len(pages), pages_per_sheet):
sheet = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
if bg_page:
sheet.merge_page(bg_page)
grid_positions = []
for slot in range(pages_per_sheet):
page_idx = sheet_idx + slot
if page_idx >= len(pages):
break
page = pages[page_idx]
page_w, page_h = get_page_size(page)
col = slot % cols
row = slot // cols
grid_positions.append((col, row))
# Position de la cellule
x = margin_left + col * (cell_w + gutter_h)
y = sheet_h - margin_top - (row + 1) * cell_h - row * gutter_v
# Calcul du scale
if scale_mode == "fit":
scale_x = cell_w / page_w
scale_y = cell_h / page_h
scale = min(scale_x, scale_y)
else:
scale = 1.0
# Centrage dans la cellule
scaled_w = page_w * scale
scaled_h = page_h * scale
offset_x = x + (cell_w - scaled_w) / 2
offset_y = y + (cell_h - scaled_h) / 2
# Placer la page
overlay = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
overlay.merge_page(page)
overlay.add_transformation(
Transformation().scale(scale, scale).translate(offset_x, offset_y)
)
sheet.merge_page(overlay)
# Reperes de coupe
if crop_marks and grid_positions:
marks = smart_crop_marks(
sheet_w, sheet_h, grid_positions,
cell_w + gutter_h, cell_h + gutter_v,
margin_left, margin_top
)
sheet.merge_page(marks)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

View File

@@ -0,0 +1,89 @@
"""Moteur Numerotation de pages."""
from pypdf import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from .pdf_utils import get_page_size
import io
POSITIONS = {
"haut-gauche": ("left", "top"),
"haut-centre": ("center", "top"),
"haut-droite": ("right", "top"),
"milieu-gauche": ("left", "middle"),
"milieu-centre": ("center", "middle"),
"milieu-droite": ("right", "middle"),
"bas-gauche": ("left", "bottom"),
"bas-centre": ("center", "bottom"),
"bas-droite": ("right", "bottom"),
}
def add_page_numbers(input_path, output_path, position="bas-centre",
font_name="Helvetica", font_size=10, color=(0, 0, 0),
prefix="", suffix="", start_number=1,
margin_x=10*mm, margin_y=10*mm,
apply_to="all"):
"""Ajouter des numeros de page.
position: cle de POSITIONS
apply_to: "all", "even", "odd"
"""
reader = PdfReader(input_path)
writer = PdfWriter()
h_align, v_align = POSITIONS.get(position, ("center", "bottom"))
total_pages = len(reader.pages)
for i, page in enumerate(reader.pages):
page_num = start_number + i
# Filtre pair/impair
if apply_to == "even" and page_num % 2 != 0:
writer.add_page(page)
continue
if apply_to == "odd" and page_num % 2 == 0:
writer.add_page(page)
continue
w, h = get_page_size(page)
# Generer le texte
text = f"{prefix}{page_num}{suffix}"
# Creer overlay avec le numero
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(w, h))
c.setFont(font_name, font_size)
c.setFillColorRGB(*color)
# Calculer la position
text_width = c.stringWidth(text, font_name, font_size)
if h_align == "left":
x = margin_x
elif h_align == "right":
x = w - margin_x - text_width
else:
x = (w - text_width) / 2
if v_align == "top":
y = h - margin_y - font_size
elif v_align == "middle":
y = (h - font_size) / 2
else:
y = margin_y
c.drawString(x, y, text)
c.showPage()
c.save()
packet.seek(0)
overlay_reader = PdfReader(packet)
page.merge_page(overlay_reader.pages[0])
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

143
app/engine/pdf_utils.py Normal file
View File

@@ -0,0 +1,143 @@
"""Utilitaires PDF communs pour Copydev Imposing Tool."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm, inch
from reportlab.lib.pagesizes import A4, A3, letter
import io
import math
# Tailles de pages predefinies (largeur, hauteur en points)
PAGE_SIZES = {
"A4 Portrait": (595.28, 841.89),
"A4 Paysage": (841.89, 595.28),
"A3 Portrait": (841.89, 1190.55),
"A3 Paysage": (1190.55, 841.89),
"Letter Portrait": (612, 792),
"Letter Paysage": (792, 612),
"Legal Portrait": (612, 1008),
"Legal Paysage": (1008, 612),
}
# Conversion
PT_TO_MM = 25.4 / 72
MM_TO_PT = 72 / 25.4
def mm_to_pt(val_mm):
"""Convertir millimetres en points."""
return val_mm * MM_TO_PT
def pt_to_mm(val_pt):
"""Convertir points en millimetres."""
return val_pt * PT_TO_MM
def get_page_size(page):
"""Obtenir la taille d'une page (largeur, hauteur) en points."""
box = page.mediabox
w = float(box.width)
h = float(box.height)
return w, h
def create_blank_page(width, height):
"""Creer une page blanche avec les dimensions donnees (en points)."""
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(width, height))
c.showPage()
c.save()
packet.seek(0)
reader = PdfReader(packet)
return reader.pages[0]
def merge_page_onto(base_page, overlay_page, x=0, y=0, scale=1.0, rotation=0):
"""Fusionner une page overlay sur une page de base a une position donnee."""
if rotation:
overlay_page = overlay_page.rotate(rotation)
transform = Transformation().scale(scale, scale).translate(x, y)
overlay_page.add_transformation(transform)
base_page.merge_page(overlay_page)
return base_page
def create_crop_marks_page(width, height, positions, mark_length=14.17, mark_offset=7.09, line_width=0.25):
"""Creer une page avec des reperes de coupe aux positions donnees.
positions: liste de tuples (x, y) ou placer les reperes
mark_length: longueur des traits (defaut 5mm = 14.17pt)
mark_offset: distance du bord de la page (defaut 2.5mm = 7.09pt)
"""
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=(width, height))
c.setLineWidth(line_width)
c.setStrokeColorRGB(0, 0, 0)
for x, y in positions:
# Trait horizontal gauche
c.line(x - mark_offset - mark_length, y, x - mark_offset, y)
# Trait horizontal droit
c.line(x + mark_offset, y, x + mark_offset + mark_length, y)
# Trait vertical bas
c.line(x, y - mark_offset - mark_length, x, y - mark_offset)
# Trait vertical haut
c.line(x, y + mark_offset, x, y + mark_offset + mark_length)
c.showPage()
c.save()
packet.seek(0)
reader = PdfReader(packet)
return reader.pages[0]
def smart_crop_marks(sheet_width, sheet_height, grid_positions, cell_width, cell_height,
margin_left=0, margin_top=0, mark_length=14.17, mark_offset=7.09):
"""Generer des reperes de coupe intelligents pour une grille n-up.
Evite les chevauchements avec le contenu des pages.
"""
positions = set()
for col, row in grid_positions:
x = margin_left + col * cell_width
y = sheet_height - margin_top - row * cell_height
# 4 coins de chaque cellule
corners = [
(x, y),
(x + cell_width, y),
(x, y - cell_height),
(x + cell_width, y - cell_height),
]
for cx, cy in corners:
positions.add((cx, cy))
return create_crop_marks_page(sheet_width, sheet_height, list(positions),
mark_length, mark_offset)
def pad_to_multiple(reader, multiple=4):
"""Ajouter des pages blanches pour atteindre un multiple donne."""
num_pages = len(reader.pages)
remainder = num_pages % multiple
if remainder == 0:
return reader
pages_to_add = multiple - remainder
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
last_page = reader.pages[-1]
w, h = get_page_size(last_page)
for _ in range(pages_to_add):
writer.add_blank_page(w, h)
output = io.BytesIO()
writer.write(output)
output.seek(0)
return PdfReader(output)

66
app/engine/step_repeat.py Normal file
View File

@@ -0,0 +1,66 @@
"""Moteur Step & Repeat : dupliquer une page plusieurs fois sur une feuille."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size, smart_crop_marks
import math
def step_and_repeat(input_path, output_path, sheet_size=(841.89, 595.28),
copies_h=None, copies_v=None, spacing_h=0, spacing_v=0,
crop_marks=False):
"""Dupliquer chaque page sur une feuille.
copies_h, copies_v: nombre de copies horizontales/verticales (None = auto)
spacing_h, spacing_v: espacement en points
"""
reader = PdfReader(input_path)
sheet_w, sheet_h = sheet_size
writer = PdfWriter()
for page in reader.pages:
page_w, page_h = get_page_size(page)
# Auto-calcul du nombre de copies
if copies_h is None:
copies_h_calc = max(1, int((sheet_w + spacing_h) / (page_w + spacing_h)))
else:
copies_h_calc = copies_h
if copies_v is None:
copies_v_calc = max(1, int((sheet_h + spacing_v) / (page_h + spacing_v)))
else:
copies_v_calc = copies_v
# Centrage sur la feuille
total_w = copies_h_calc * page_w + (copies_h_calc - 1) * spacing_h
total_h = copies_v_calc * page_h + (copies_v_calc - 1) * spacing_v
start_x = (sheet_w - total_w) / 2
start_y = (sheet_h - total_h) / 2
sheet = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
grid_positions = []
for row in range(copies_v_calc):
for col in range(copies_h_calc):
x = start_x + col * (page_w + spacing_h)
y = start_y + (copies_v_calc - 1 - row) * (page_h + spacing_v)
overlay = PageObject.create_blank_page(width=sheet_w, height=sheet_h)
overlay.merge_page(page)
overlay.add_transformation(Transformation().translate(x, y))
sheet.merge_page(overlay)
grid_positions.append((col, row))
if crop_marks and grid_positions:
marks = smart_crop_marks(
sheet_w, sheet_h, grid_positions,
page_w + spacing_h, page_h + spacing_v,
start_x, sheet_h - start_y - total_h
)
sheet.merge_page(marks)
writer.add_page(sheet)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

341
app/main_window.py Normal file
View File

@@ -0,0 +1,341 @@
"""Fenetre principale de Copydev Imposing Tool."""
import os
import tempfile
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFileDialog, QMessageBox,
QGroupBox, QStatusBar, QMenuBar, QSplitter,
QProgressBar)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction
from app.pdf_viewer import PdfViewer
from app.dialogs.booklet_dialog import BookletDialog
from app.dialogs.nup_dialog import NupDialog
from app.dialogs.step_repeat_dialog import StepRepeatDialog
from app.dialogs.manual_impose_dialog import ManualImposeDialog
from app.dialogs.join_pages_dialog import JoinPagesDialog
from app.dialogs.insert_blank_dialog import InsertBlankDialog
from app.dialogs.page_numbers_dialog import PageNumbersDialog
from app.dialogs.masking_dialog import MaskingDialog
from app.dialogs.bleeds_dialog import BleedsDialog
from app.engine.booklet import create_booklet
from app.engine.nup import nup_pages
from app.engine.step_repeat import step_and_repeat
from app.engine.manual_impose import manual_impose
from app.engine.join_pages import join_pages
from app.engine.insert_blank import insert_blank_pages
from app.engine.page_numbers import add_page_numbers
from app.engine.masking import apply_masking
from app.engine.bleeds import define_bleeds
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Copydev Imposing Tool")
self.setMinimumSize(1000, 700)
self._current_file = None
self._result_file = None
self._setup_menu()
self._setup_ui()
self._setup_statusbar()
def _setup_menu(self):
menubar = self.menuBar()
# Menu Fichier
file_menu = menubar.addMenu("Fichier")
open_act = QAction("Ouvrir PDF...", self)
open_act.setShortcut("Ctrl+O")
open_act.triggered.connect(self._open_file)
file_menu.addAction(open_act)
save_act = QAction("Enregistrer le resultat...", self)
save_act.setShortcut("Ctrl+S")
save_act.triggered.connect(self._save_result)
file_menu.addAction(save_act)
file_menu.addSeparator()
quit_act = QAction("Quitter", self)
quit_act.setShortcut("Ctrl+Q")
quit_act.triggered.connect(self.close)
file_menu.addAction(quit_act)
# Menu Aide
help_menu = menubar.addMenu("Aide")
about_act = QAction("A propos...", self)
about_act.triggered.connect(self._show_about)
help_menu.addAction(about_act)
def _setup_ui(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
# === Panneau gauche : boutons ===
left_panel = QWidget()
left_panel.setFixedWidth(200)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(5, 5, 5, 5)
# Ouvrir
btn_open = QPushButton("Ouvrir PDF...")
btn_open.clicked.connect(self._open_file)
left_layout.addWidget(btn_open)
btn_save = QPushButton("Enregistrer resultat...")
btn_save.clicked.connect(self._save_result)
left_layout.addWidget(btn_save)
left_layout.addSpacing(10)
# Imposition
grp_impose = QGroupBox("Imposition")
il = QVBoxLayout(grp_impose)
btn_booklet = QPushButton("Creer un livret")
btn_booklet.clicked.connect(self._do_booklet)
il.addWidget(btn_booklet)
btn_nup = QPushButton("N pages par feuille")
btn_nup.clicked.connect(self._do_nup)
il.addWidget(btn_nup)
btn_repeat = QPushButton("Repetition")
btn_repeat.clicked.connect(self._do_step_repeat)
il.addWidget(btn_repeat)
btn_manual = QPushButton("Imposition manuelle")
btn_manual.clicked.connect(self._do_manual_impose)
il.addWidget(btn_manual)
btn_join = QPushButton("Joindre deux pages")
btn_join.clicked.connect(self._do_join_pages)
il.addWidget(btn_join)
left_layout.addWidget(grp_impose)
# Pages
grp_pages = QGroupBox("Pages")
pl = QVBoxLayout(grp_pages)
btn_blank = QPushButton("Pages blanches")
btn_blank.clicked.connect(self._do_insert_blank)
pl.addWidget(btn_blank)
left_layout.addWidget(grp_pages)
# Annotations
grp_annot = QGroupBox("Annotations")
al = QVBoxLayout(grp_annot)
btn_numbers = QPushButton("Numerotation")
btn_numbers.clicked.connect(self._do_page_numbers)
al.addWidget(btn_numbers)
btn_mask = QPushButton("Masquage")
btn_mask.clicked.connect(self._do_masking)
al.addWidget(btn_mask)
btn_bleeds = QPushButton("Fonds perdus")
btn_bleeds.clicked.connect(self._do_bleeds)
al.addWidget(btn_bleeds)
left_layout.addWidget(grp_annot)
left_layout.addStretch()
main_layout.addWidget(left_panel)
# === Zone centrale : viewer PDF ===
self._viewer = PdfViewer()
self._viewer.page_changed.connect(self._on_page_changed)
main_layout.addWidget(self._viewer, 1)
def _setup_statusbar(self):
self._statusbar = QStatusBar()
self.setStatusBar(self._statusbar)
self._status_label = QLabel("Pret")
self._statusbar.addWidget(self._status_label, 1)
self._file_label = QLabel("")
self._statusbar.addPermanentWidget(self._file_label)
def _on_page_changed(self, page, total):
pass
def _open_file(self):
path, _ = QFileDialog.getOpenFileName(
self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)"
)
if path:
self._current_file = path
self._result_file = None
self._viewer.load_pdf(path)
self._file_label.setText(os.path.basename(path))
self._status_label.setText(f"Fichier charge : {os.path.basename(path)}")
def _save_result(self):
source = self._result_file or self._current_file
if not source:
QMessageBox.warning(self, "Erreur", "Aucun fichier a enregistrer.")
return
path, _ = QFileDialog.getSaveFileName(
self, "Enregistrer le resultat", "", "PDF (*.pdf)"
)
if path:
if not path.lower().endswith(".pdf"):
path += ".pdf"
import shutil
shutil.copy2(source, path)
self._status_label.setText(f"Enregistre : {os.path.basename(path)}")
def _require_file(self):
if not self._current_file:
QMessageBox.warning(self, "Erreur", "Veuillez d'abord ouvrir un fichier PDF.")
return False
return True
def _make_temp(self):
fd, path = tempfile.mkstemp(suffix=".pdf")
os.close(fd)
return path
def _show_result(self, output_path):
self._result_file = output_path
self._viewer.load_pdf(output_path)
self._status_label.setText("Traitement termine")
def _do_booklet(self):
if not self._require_file():
return
dlg = BookletDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
create_booklet(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_nup(self):
if not self._require_file():
return
dlg = NupDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
nup_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_step_repeat(self):
if not self._require_file():
return
dlg = StepRepeatDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
step_and_repeat(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_manual_impose(self):
if not self._require_file():
return
from pypdf import PdfReader
reader = PdfReader(self._current_file)
dlg = ManualImposeDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
manual_impose(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_join_pages(self):
if not self._require_file():
return
dlg = JoinPagesDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
join_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_insert_blank(self):
if not self._require_file():
return
from pypdf import PdfReader
reader = PdfReader(self._current_file)
dlg = InsertBlankDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
insert_blank_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_page_numbers(self):
if not self._require_file():
return
dlg = PageNumbersDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
add_page_numbers(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_masking(self):
if not self._require_file():
return
dlg = MaskingDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
apply_masking(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _do_bleeds(self):
if not self._require_file():
return
dlg = BleedsDialog(self)
if dlg.exec():
settings = dlg.get_settings()
output = self._make_temp()
try:
define_bleeds(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
QMessageBox.critical(self, "Erreur", str(e))
def _show_about(self):
QMessageBox.about(
self,
"A propos",
"Copydev Imposing Tool\n\n"
"Outil d'imposition PDF\n"
"Remplace Quite Imposing Plus\n\n"
"Copydev 2026"
)

178
app/pdf_viewer.py Normal file
View File

@@ -0,0 +1,178 @@
"""Widget de visualisation PDF avec pypdfium2."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSpinBox, QScrollArea, QSizePolicy)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtCore import Qt, pyqtSignal
import pypdfium2 as pdfium
class PdfViewer(QWidget):
"""Widget d'apercu PDF avec navigation et zoom."""
page_changed = pyqtSignal(int, int) # page_num, total_pages
def __init__(self, parent=None):
super().__init__(parent)
self._pdf = None
self._current_page = 0
self._total_pages = 0
self._zoom = 1.0
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Zone d'affichage avec scroll
self._scroll = QScrollArea()
self._scroll.setWidgetResizable(True)
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._image_label = QLabel()
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
self._scroll.setWidget(self._image_label)
layout.addWidget(self._scroll, 1)
# Barre de navigation
nav = QHBoxLayout()
self._btn_prev = QPushButton("<")
self._btn_prev.setFixedWidth(40)
self._btn_prev.clicked.connect(self._prev_page)
nav.addWidget(self._btn_prev)
self._page_spin = QSpinBox()
self._page_spin.setMinimum(1)
self._page_spin.setMaximum(1)
self._page_spin.valueChanged.connect(self._go_to_page)
nav.addWidget(self._page_spin)
self._label_total = QLabel("/ 0")
nav.addWidget(self._label_total)
self._btn_next = QPushButton(">")
self._btn_next.setFixedWidth(40)
self._btn_next.clicked.connect(self._next_page)
nav.addWidget(self._btn_next)
nav.addSpacing(20)
self._btn_zoom_out = QPushButton("-")
self._btn_zoom_out.setFixedWidth(30)
self._btn_zoom_out.clicked.connect(self._zoom_out)
nav.addWidget(self._btn_zoom_out)
self._label_zoom = QLabel("100%")
self._label_zoom.setFixedWidth(45)
self._label_zoom.setAlignment(Qt.AlignmentFlag.AlignCenter)
nav.addWidget(self._label_zoom)
self._btn_zoom_in = QPushButton("+")
self._btn_zoom_in.setFixedWidth(30)
self._btn_zoom_in.clicked.connect(self._zoom_in)
nav.addWidget(self._btn_zoom_in)
self._btn_fit = QPushButton("Ajuster")
self._btn_fit.clicked.connect(self._zoom_fit)
nav.addWidget(self._btn_fit)
nav.addStretch()
layout.addLayout(nav)
def load_pdf(self, path):
"""Charger un fichier PDF."""
try:
self._pdf = pdfium.PdfDocument(path)
self._total_pages = len(self._pdf)
self._current_page = 0
self._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
def load_pdf_bytes(self, data):
"""Charger un PDF depuis des bytes."""
try:
self._pdf = pdfium.PdfDocument(data)
self._total_pages = len(self._pdf)
self._current_page = 0
self._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
def _render_page(self):
"""Afficher la page courante."""
if not self._pdf or self._current_page >= self._total_pages:
return
page = self._pdf[self._current_page]
# Rendu a 150 DPI * zoom
scale = 150 / 72 * self._zoom
bitmap = page.render(scale=scale)
pil_image = bitmap.to_pil()
# Convertir PIL -> QImage -> QPixmap
data = pil_image.tobytes("raw", "RGBA")
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
pixmap = QPixmap.fromImage(qimage)
self._image_label.setPixmap(pixmap)
self._image_label.adjustSize()
self._label_zoom.setText(f"{int(self._zoom * 100)}%")
self.page_changed.emit(self._current_page + 1, self._total_pages)
def _prev_page(self):
if self._current_page > 0:
self._current_page -= 1
self._page_spin.setValue(self._current_page + 1)
def _next_page(self):
if self._current_page < self._total_pages - 1:
self._current_page += 1
self._page_spin.setValue(self._current_page + 1)
def _go_to_page(self, value):
page = value - 1
if 0 <= page < self._total_pages and page != self._current_page:
self._current_page = page
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.25)
self._render_page()
def _zoom_fit(self):
"""Ajuster le zoom pour que la page tienne dans la zone visible."""
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
scale_x = vw / pw
scale_y = vh / ph
self._zoom = min(scale_x, scale_y) * (72 / 150)
self._render_page()
def refresh(self):
"""Rafraichir l'affichage."""
self._render_page()
@property
def current_page(self):
return self._current_page
@property
def total_pages(self):
return self._total_pages

0
app/presets/__init__.py Normal file
View File