It did it in less than a minute!
Now each time I have to place several images in one PDF or merge multiple PDFs I can simply call the script with an alias:
Alias PDFmerge -RunPythonScript (C:\RhinoPythonScripts\RhinoPDFmerge.py)
This is a good example of automating boring stuff.
#! python 3
# requirements: pypdf
# requirements: pillow
#!/usr/bin/env python3
# Combine PDFs and images into a single PDF (Rhino-safe, no CLI args)
# Requires: pypdf, Pillow (PIL).
import os, io
import rhinoscriptsyntax as rs
# --- third-party libs ---
from pypdf import PdfReader, PdfWriter
from PIL import Image, ImageSequence
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
PDF_EXT = ".pdf"
def is_image(path):
return os.path.splitext(path.lower())[1] in IMAGE_EXTS
def is_pdf(path):
return path.lower().endswith(PDF_EXT)
def image_frames_to_pdf_bytes(img_path):
"""Return list of single-page PDF bytes (handles multi-page TIFFs)."""
pages = []
with Image.open(img_path) as im:
try:
frames = [frame.copy() for frame in ImageSequence.Iterator(im)]
if not frames:
frames = [im.copy()]
except Exception:
frames = [im.copy()]
for frame in frames:
# Flatten transparency, ensure RGB/L
if frame.mode in ("RGBA", "LA"):
bg = Image.new("RGB", frame.size, (255, 255, 255))
bg.paste(frame, mask=frame.split()[-1])
frame = bg
elif frame.mode not in ("RGB", "L"):
frame = frame.convert("RGB")
bio = io.BytesIO()
# Pillow embeds as a one-page PDF (size from pixels; set resolution for better print metadata)
frame.save(bio, format="PDF", resolution=300)
pages.append(bio.getvalue())
return pages
def append_pdf_bytes(writer, pdf_bytes):
reader = PdfReader(io.BytesIO(pdf_bytes))
for page in reader.pages:
writer.add_page(page)
def combine_files_to_pdf(files, output_pdf):
if not files:
rs.MessageBox("No files selected.", 48, "Combine to PDF")
return False
writer = PdfWriter()
added = 0
for path in files:
ext = os.path.splitext(path)[1].lower()
if is_pdf(path):
try:
reader = PdfReader(path)
for page in reader.pages:
writer.add_page(page)
added += len(reader.pages)
except Exception as e:
print("Skipped (PDF read error):", path, "-", e)
elif is_image(path):
try:
for pdf_bytes in image_frames_to_pdf_bytes(path):
append_pdf_bytes(writer, pdf_bytes)
added += 1
except Exception as e:
print("Skipped (image error):", path, "-", e)
else:
print("Skipped (unsupported type):", path)
if added == 0:
rs.MessageBox("Nothing was added. Check file types.", 48, "Combine to PDF")
return False
# Write output
with open(output_pdf, "wb") as f:
writer.write(f)
print("Done. Wrote:", output_pdf, "| pages:", added)
rs.MessageBox("Created:\n{}".format(output_pdf), 64, "Combine to PDF")
return True
def pick_files():
# Multi-select files (PDFs + common image types)
flt = "PDF and images (*.pdf;*.jpg;*.jpeg;*.png;*.bmp;*.tif;*.tiff;*.webp)|*.pdf;*.jpg;*.jpeg;*.png;*.bmp;*.tif;*.tiff;*.webp|All files (*.*)|*.*||"
files = rs.OpenFileNames("Select PDFs and/or images (order is kept as selected folder view)", filter=flt)
if not files:
return []
# Optional: natural-sort by name; or keep dialog order. Here we keep dialog order.
return list(files)
def pick_output(default_name="combined.pdf"):
return rs.SaveFileName("Save combined PDF as", filter="PDF (*.pdf)|*.pdf||", filename=default_name)
# --- MAIN ---
if __name__ == "__main__":
# Option A: set FILES manually and skip the picker
FILES = [] # e.g. ["C:\\temp\\scan1.jpg", "C:\\temp\\doc.pdf"]
if not FILES:
FILES = pick_files()
if not FILES:
raise SystemExit
out = pick_output()
if not out:
raise SystemExit
if not out.lower().endswith(".pdf"):
out += ".pdf"
# Confirm overwrite (Rhino doesn't warn by default)
if os.path.exists(out):
if rs.MessageBox("File exists. Overwrite?\n{}".format(out), 33, "Confirm overwrite") != 1:
raise SystemExit
combine_files_to_pdf(FILES, out)
Check out other scripts here: