Files
2026-03-16 18:50:45 +01:00

116 lines
4.4 KiB
Python
Executable File

import argparse
import fitz # PyMuPDF
def side_by_side_preserve_links(input_pdf: str, output_pdf: str, start_page: int = 1):
src = fitz.open(input_pdf)
out = fitz.open()
# Map each source page -> (output_page_index, x_offset, y_offset)
# In this simple layout:
# - even source pages go on the LEFT at x_offset=0
# - odd source pages go on the RIGHT at x_offset=width_of_left_page (of that pair)
placement = {}
# 1) Create output pages and place page content, while recording placement
out_page_index = 0
i = start_page - 1
while i < src.page_count:
left = src.load_page(i)
left_rect = left.rect
right = src.load_page(i + 1) if (i + 1) < src.page_count else None
right_rect = right.rect if right else fitz.Rect(0, 0, 0, 0)
new_width = left_rect.width + (right_rect.width if right else 0) - 1
new_height = max(left_rect.height, right_rect.height if right else 0)
new_page = out.new_page(width=new_width, height=new_height)
# Show left page
left_target = fitz.Rect(0, 0, left_rect.width, left_rect.height)
new_page.show_pdf_page(left_target, src, i)
placement[i] = (out_page_index, 0.0, 0.0)
# Show right page
if right:
right_x = left_rect.width - 1
right_target = fitz.Rect(right_x, 0, right_x + right_rect.width, right_rect.height)
new_page.show_pdf_page(right_target, src, i + 1)
placement[i + 1] = (out_page_index, right_x, 0.0)
out_page_index += 1
i += 2
# Helper: map a source destination (page + point) into the 2-up output coordinate space
def map_dest(src_page_num: int, pt: fitz.Point | None):
# Destination output page index
dest_out_page, dest_xoff, dest_yoff = placement[src_page_num]
if pt is None:
return dest_out_page, None
return dest_out_page, fitz.Point(pt.x + dest_xoff, pt.y + dest_yoff)
# Helper: map a source link rectangle ("from") into the output coordinate space
def map_from_rect(src_page_num: int, r: fitz.Rect):
out_page_num, xoff, yoff = placement[src_page_num]
return out_page_num, fitz.Rect(r.x0 + xoff, r.y0 + yoff, r.x1 + xoff, r.y1 + yoff)
# 2) Copy links (annotations) from each source page to the correct output page
for spn in range(src.page_count):
links = src.load_page(spn).get_links()
if not links:
continue
for lnk in links:
# Map the clickable rectangle
out_pn, new_from = map_from_rect(spn, lnk["from"])
out_page = out.load_page(out_pn)
kind = lnk.get("kind")
# Common fields
new_link = {
"kind": kind,
"from": new_from,
}
# External URL links
if kind == fitz.LINK_URI:
new_link["uri"] = lnk.get("uri", "")
# Internal "go to page" links
elif kind == fitz.LINK_GOTO:
dest_src_page = lnk.get("page")
if dest_src_page is None or dest_src_page < 0 or dest_src_page >= src.page_count:
continue # skip broken destinations
dest_out_page, new_to = map_dest(dest_src_page, lnk.get("to"))
new_link["page"] = dest_out_page
if new_to is not None:
new_link["to"] = new_to
if "zoom" in lnk and lnk["zoom"] is not None:
new_link["zoom"] = lnk["zoom"]
# Named destinations / file launches etc.
# You can add more cases if you use them, but most PDFs are URI + GOTO.
else:
# Try a best-effort copy for other kinds (may or may not be supported)
for k in ("page", "to", "zoom", "file", "uri", "name"):
if k in lnk and lnk[k] is not None:
new_link[k] = lnk[k]
out_page.insert_link(new_link)
out.save(output_pdf)
out.close()
src.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Places pages side by side")
parser.add_argument("input_pdf", help="Path to the source PDF file.")
parser.add_argument("output_pdf", help="Path to the destination PDF file.")
parser.add_argument("start_page", help="Allows to skip n number of pages", type=int)
args = parser.parse_args()
side_by_side_preserve_links(args.input_pdf, args.output_pdf, args.start_page)