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