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:
Jules
2026-03-21 16:02:07 +01:00
parent cee86f5c03
commit 673d76caa2
7 changed files with 364 additions and 12 deletions

59
app/engine/trim_shift.py Normal file
View File

@@ -0,0 +1,59 @@
"""Moteur Trim & Shift — decaler le contenu des pages."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size
def trim_and_shift(input_path, output_path, shift_x=0, shift_y=0,
shift_x_even=None, shift_y_even=None,
creep=0, creep_pages=0):
"""Decaler le contenu des pages.
shift_x, shift_y: decalage en points (pages impaires si even specifie)
shift_x_even, shift_y_even: decalage pour pages paires (None = meme que impaires)
creep: decalage progressif en points (compensation epaisseur papier)
creep_pages: nombre total de pages pour le calcul du creep
"""
reader = PdfReader(input_path)
writer = PdfWriter()
num_pages = len(reader.pages)
if shift_x_even is None:
shift_x_even = shift_x
if shift_y_even is None:
shift_y_even = shift_y
for i, page in enumerate(reader.pages):
page_num = i + 1
w, h = get_page_size(page)
# Decalage de base
if page_num % 2 == 0:
dx, dy = shift_x_even, shift_y_even
else:
dx, dy = shift_x, shift_y
# Creep progressif (pour livrets)
if creep != 0 and creep_pages > 0:
# Les pages du centre se decalent le plus
mid = num_pages / 2
distance_from_center = abs(i - mid) / mid # 0 au centre, 1 aux bords
creep_amount = creep * (1 - distance_from_center)
if page_num % 2 == 0:
dx -= creep_amount # pages paires : decaler vers la gauche
else:
dx += creep_amount # pages impaires : decaler vers la droite
if dx != 0 or dy != 0:
new_page = PageObject.create_blank_page(width=w, height=h)
overlay = PageObject.create_blank_page(width=w, height=h)
overlay.merge_page(page)
overlay.add_transformation(Transformation().translate(dx, dy))
new_page.merge_page(overlay)
writer.add_page(new_page)
else:
writer.add_page(page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path

73
app/engine/unbooklet.py Normal file
View File

@@ -0,0 +1,73 @@
"""Moteur Delivretiser — convertir un livret en pages sequentielles."""
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
from .pdf_utils import get_page_size
import math
def unbooklet(input_path, output_path):
"""Delivretiser un PDF en format livret.
Chaque page du livret contient 2 pages cote a cote.
On les coupe en 2 et on les remet dans l'ordre sequentiel.
"""
reader = PdfReader(input_path)
num_sheets = len(reader.pages)
total_pages = num_sheets * 2
# Extraire toutes les demi-pages
half_pages = [] # (sheet_index, "left" ou "right")
for i in range(num_sheets):
half_pages.append((i, "left"))
half_pages.append((i, "right"))
# Ordre du livret (piqure a cheval) :
# Feuille 0 recto : page N, page 1
# Feuille 0 verso : page 2, page N-1
# Feuille 1 recto : page N-2, page 3
# etc.
# On reconstruit l'ordre
page_map = {} # page_number (1-based) -> (sheet_index, side)
n = total_pages
for i in range(num_sheets):
if i % 2 == 0:
# Recto : gauche = N - i, droite = i + 1
page_map[n - i] = (i, "left")
page_map[i + 1] = (i, "right")
else:
# Verso : gauche = i + 1, droite = N - i
page_map[i + 1] = (i, "left")
page_map[n - i] = (i, "right")
writer = PdfWriter()
for page_num in range(1, total_pages + 1):
if page_num not in page_map:
continue
sheet_idx, side = page_map[page_num]
sheet = reader.pages[sheet_idx]
sheet_w, sheet_h = get_page_size(sheet)
half_w = sheet_w / 2
# Creer une nouvelle page avec la moitie
new_page = PageObject.create_blank_page(width=half_w, height=sheet_h)
if side == "left":
# Copier la moitie gauche
overlay = PageObject.create_blank_page(width=half_w, height=sheet_h)
overlay.merge_page(sheet)
overlay.mediabox.upper_right = (half_w, sheet_h)
new_page.merge_page(overlay)
else:
# Copier la moitie droite
overlay = PageObject.create_blank_page(width=half_w, height=sheet_h)
overlay.merge_page(sheet)
overlay.add_transformation(Transformation().translate(-half_w, 0))
overlay.mediabox.upper_right = (half_w, sheet_h)
new_page.merge_page(overlay)
writer.add_page(new_page)
with open(output_path, "wb") as f:
writer.write(f)
return output_path