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/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