V3.0.0 — Rotation, Annuler/Refaire, Delivretiser, Trim&Shift, Inverser
- Rotation de pages : clic droit sur vignette → Pivoter +90/-90/180 Aussi dans le menu Pages - Ctrl+Z / Ctrl+Y : annuler et refaire (pile de 20 etats) - Delivretiser : coupe les pages en 2 et remet dans l'ordre sequentiel (inverse de la creation de livret) - Trim & Shift : decaler le contenu des pages - Decalage X/Y different pages paires/impaires - Compensation de creep progressive pour livrets epais - Inverser l'ordre des pages (menu Pages) - Fix impression : ouvre le PDF dans le lecteur par defaut - Barre outils droite : ajout Delivretiser, Trim/Shift, Inverser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,9 @@ from app.engine.bleeds import define_bleeds
|
||||
from app.engine.crop_marks import add_crop_marks, remove_crop_marks
|
||||
from app.engine.crop import crop_pages
|
||||
from app.engine.add_content import add_text_to_pages, add_image_to_pages
|
||||
from app.engine.unbooklet import unbooklet
|
||||
from app.engine.trim_shift import trim_and_shift
|
||||
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -56,8 +59,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._current_file = None
|
||||
self._result_file = None
|
||||
self._original_file = None # fichier avant crop (pour recadrage non-destructif)
|
||||
self._crop_settings = {} # {page_index: {top, bottom, left, right} en pt}
|
||||
self._original_file = None
|
||||
self._crop_settings = {}
|
||||
self._undo_stack = [] # pile de fichiers pour annuler
|
||||
self._redo_stack = []
|
||||
|
||||
# Drag & drop
|
||||
self.setAcceptDrops(True)
|
||||
@@ -200,6 +205,11 @@ class MainWindow(QMainWindow):
|
||||
file_menu.addSeparator()
|
||||
self._add_action(file_menu, "Quitter", "Ctrl+Q", self.close)
|
||||
|
||||
# Edition
|
||||
edit_menu = menubar.addMenu("Edition")
|
||||
self._add_action(edit_menu, "Annuler", "Ctrl+Z", self._do_undo)
|
||||
self._add_action(edit_menu, "Refaire", "Ctrl+Y", self._do_redo)
|
||||
|
||||
# Imposition
|
||||
impose_menu = menubar.addMenu("Imposition")
|
||||
self._add_action(impose_menu, "Creer un livret", None, self._do_booklet)
|
||||
@@ -207,10 +217,18 @@ class MainWindow(QMainWindow):
|
||||
self._add_action(impose_menu, "Repetition (Step && Repeat)", None, self._do_step_repeat)
|
||||
self._add_action(impose_menu, "Imposition manuelle", None, self._do_manual_impose)
|
||||
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
|
||||
impose_menu.addSeparator()
|
||||
self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet)
|
||||
|
||||
# Pages
|
||||
pages_menu = menubar.addMenu("Pages")
|
||||
self._add_action(pages_menu, "Recadrer...", None, self._do_crop)
|
||||
self._add_action(pages_menu, "Trim && Shift...", None, self._do_trim_shift)
|
||||
pages_menu.addSeparator()
|
||||
self._add_action(pages_menu, "Pivoter +90", None, lambda: self._rotate_current(90))
|
||||
self._add_action(pages_menu, "Pivoter -90", None, lambda: self._rotate_current(-90))
|
||||
self._add_action(pages_menu, "Pivoter 180", None, lambda: self._rotate_current(180))
|
||||
self._add_action(pages_menu, "Inverser l'ordre des pages", None, self._do_reverse_pages)
|
||||
pages_menu.addSeparator()
|
||||
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
|
||||
|
||||
@@ -284,6 +302,7 @@ class MainWindow(QMainWindow):
|
||||
# Section Imposition
|
||||
rtb.addWidget(self._make_section_label("IMPOSITION"))
|
||||
rtb.addAction("Livret", self._do_booklet)
|
||||
rtb.addAction("Delivretiser", self._do_unbooklet)
|
||||
rtb.addAction("N-Up", self._do_nup)
|
||||
rtb.addAction("Repetition", self._do_step_repeat)
|
||||
rtb.addAction("Manuel", self._do_manual_impose)
|
||||
@@ -293,7 +312,9 @@ class MainWindow(QMainWindow):
|
||||
# Section Pages
|
||||
rtb.addWidget(self._make_section_label("PAGES"))
|
||||
rtb.addAction("Recadrer", self._do_crop)
|
||||
rtb.addAction("Trim/Shift", self._do_trim_shift)
|
||||
rtb.addAction("Pg blanche", self._do_insert_blank)
|
||||
rtb.addAction("Inverser", self._do_reverse_pages)
|
||||
rtb.addAction("Fusionner", self._do_merge)
|
||||
rtb.addSeparator()
|
||||
|
||||
@@ -340,6 +361,7 @@ class MainWindow(QMainWindow):
|
||||
self._page_panel.page_delete_requested.connect(self._on_page_delete)
|
||||
self._page_panel.page_insert_requested.connect(self._on_page_insert)
|
||||
self._page_panel.pages_reordered.connect(self._on_pages_reordered)
|
||||
self._page_panel.page_rotate_requested.connect(self._on_page_rotate)
|
||||
splitter.addWidget(self._page_panel)
|
||||
|
||||
# Viewer PDF
|
||||
@@ -488,6 +510,12 @@ class MainWindow(QMainWindow):
|
||||
self._progress.setRange(0, 1)
|
||||
|
||||
def _show_result(self, output_path):
|
||||
# Sauvegarder l'etat actuel pour annulation
|
||||
if self._current_file:
|
||||
self._undo_stack.append(self._current_file)
|
||||
self._redo_stack.clear()
|
||||
if len(self._undo_stack) > 20:
|
||||
self._undo_stack.pop(0)
|
||||
self._result_file = output_path
|
||||
self._current_file = output_path
|
||||
self._viewer.load_pdf(output_path)
|
||||
@@ -496,6 +524,97 @@ class MainWindow(QMainWindow):
|
||||
self._status_label.setText("Traitement termine")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
def _do_undo(self):
|
||||
if not self._undo_stack:
|
||||
return
|
||||
self._redo_stack.append(self._current_file)
|
||||
prev = self._undo_stack.pop()
|
||||
self._current_file = prev
|
||||
self._result_file = prev
|
||||
self._viewer.load_pdf(prev)
|
||||
self._page_panel.load_thumbnails(self._viewer)
|
||||
self._status_label.setText("Annule")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
def _do_redo(self):
|
||||
if not self._redo_stack:
|
||||
return
|
||||
self._undo_stack.append(self._current_file)
|
||||
nxt = self._redo_stack.pop()
|
||||
self._current_file = nxt
|
||||
self._result_file = nxt
|
||||
self._viewer.load_pdf(nxt)
|
||||
self._page_panel.load_thumbnails(self._viewer)
|
||||
self._status_label.setText("Refait")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
# ── Rotation ──
|
||||
|
||||
def _on_page_rotate(self, page_index, degrees):
|
||||
if not self._current_file:
|
||||
return
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
reader = PdfReader(self._current_file)
|
||||
writer = PdfWriter()
|
||||
for i, page in enumerate(reader.pages):
|
||||
if i == page_index:
|
||||
page = page.rotate(degrees)
|
||||
writer.add_page(page)
|
||||
output = self._make_temp()
|
||||
with open(output, "wb") as f:
|
||||
writer.write(f)
|
||||
self._show_result(output)
|
||||
|
||||
def _rotate_current(self, degrees):
|
||||
if not self._require_file():
|
||||
return
|
||||
self._on_page_rotate(self._viewer.current_page, degrees)
|
||||
|
||||
def _do_reverse_pages(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
self._show_progress()
|
||||
reader = PdfReader(self._current_file)
|
||||
writer = PdfWriter()
|
||||
for page in reversed(reader.pages):
|
||||
writer.add_page(page)
|
||||
output = self._make_temp()
|
||||
with open(output, "wb") as f:
|
||||
writer.write(f)
|
||||
self._show_result(output)
|
||||
self._status_label.setText("Ordre des pages inverse")
|
||||
|
||||
def _do_unbooklet(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
self._show_progress()
|
||||
output = self._make_temp()
|
||||
try:
|
||||
unbooklet(self._current_file, output)
|
||||
self._show_result(output)
|
||||
self._status_label.setText("Delivretisation terminee")
|
||||
except Exception as e:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _do_trim_shift(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
from pypdf import PdfReader
|
||||
reader = PdfReader(self._current_file)
|
||||
dlg = TrimShiftDialog(total_pages=len(reader.pages), parent=self)
|
||||
if dlg.exec():
|
||||
self._show_progress()
|
||||
settings = dlg.get_settings()
|
||||
output = self._make_temp()
|
||||
try:
|
||||
trim_and_shift(self._current_file, output, **settings)
|
||||
self._show_result(output)
|
||||
except Exception as e:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
# ── Actions imposition ──
|
||||
|
||||
def _do_booklet(self):
|
||||
@@ -789,14 +908,20 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
try:
|
||||
source = self._result_file or self._current_file
|
||||
# Copier dans un fichier temp avec un nom lisible
|
||||
import shutil
|
||||
tmp_print = os.path.join(tempfile.gettempdir(), "copydev_print.pdf")
|
||||
shutil.copy2(source, tmp_print)
|
||||
if sys.platform == "win32":
|
||||
os.startfile(source, "print")
|
||||
import subprocess
|
||||
# Ouvrir avec le lecteur PDF par defaut (qui a un menu imprimer)
|
||||
subprocess.Popen(["cmd", "/c", "start", "", tmp_print])
|
||||
else:
|
||||
import subprocess
|
||||
subprocess.Popen(["lp", source])
|
||||
self._status_label.setText("Impression lancee...")
|
||||
subprocess.Popen(["xdg-open", tmp_print])
|
||||
self._status_label.setText("Fichier ouvert pour impression")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", f"Impossible d'imprimer :\n{e}")
|
||||
QMessageBox.critical(self, "Erreur", f"Impossible d'ouvrir pour impression :\n{e}")
|
||||
|
||||
def _do_crop_marks(self):
|
||||
if not self._require_file():
|
||||
|
||||
Reference in New Issue
Block a user