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

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