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:
196
app/engine/booklet.py
Normal file
196
app/engine/booklet.py
Normal 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
|
||||
Reference in New Issue
Block a user