Skip to content

Instantly share code, notes, and snippets.

@nobucshirai
Last active February 18, 2025 02:22
Show Gist options
  • Save nobucshirai/d5216fe9fa1b1fb719c05c880866cd66 to your computer and use it in GitHub Desktop.
Save nobucshirai/d5216fe9fa1b1fb719c05c880866cd66 to your computer and use it in GitHub Desktop.
Image to PDF Converter: Easily combine multiple images into a single PDF file. Just provide image paths and an optional output name. Perfect for quick document assembly tasks.
#!/usr/bin/env python3
"""
Merge image files into a single PDF, optionally annotating images with their filenames using ImageMagick.
The grid layout is controlled by the number of rows and columns.
Use the --with-text flag to enable filename annotation (default is without text).
You can adjust the annotation font size by using the --font-scale option.
"""
import argparse
import os
import sys
import subprocess
import tempfile
from typing import List
from PIL import Image
def process_image(image_path: str, with_text: bool, font_scale: float) -> Image.Image:
"""
Open an image and, if with_text is True, annotate it using ImageMagick.
The font size for annotation is set by multiplying the default size (50) by font_scale.
Returns a PIL Image.
"""
if with_text:
# Calculate the new point size.
pointsize = str(int(50 * font_scale))
# Create a temporary file to store the annotated image.
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp:
temp_filename = temp.name
try:
# Use ImageMagick to add text (the filename) to the image.
subprocess.run(
[
"magick", image_path,
"-gravity", "northwest",
"-pointsize", pointsize,
"-undercolor", "white",
"-annotate", "+50+175", os.path.basename(image_path),
temp_filename
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
except subprocess.CalledProcessError as e:
print(f"Error annotating image {image_path} with magick:\n{e.stderr.decode().strip()}")
os.remove(temp_filename)
# Fallback: open the image without annotation
img = Image.open(image_path)
return img
try:
img = Image.open(temp_filename)
img.load() # Ensure the image is loaded before we remove the temp file
except Exception as e:
print(f"Error loading annotated image {temp_filename}: {e}")
img = Image.open(image_path)
finally:
os.remove(temp_filename)
return img
else:
return Image.open(image_path)
def create_image_grid(image_paths: List[str], rows: int, cols: int, with_text: bool, font_scale: float) -> Image.Image:
"""
Create a grid image from a list of image paths.
If fewer images than grid cells are provided, the remaining cells are filled with white.
Each image is optionally annotated with its filename using ImageMagick,
and when annotated, a white rectangle is inserted under the text.
The font size for annotation is adjusted by font_scale.
"""
# Open the first image to determine the cell size.
try:
first_image = process_image(image_paths[0], with_text, font_scale)
except Exception as e:
print(f"Error opening {image_paths[0]}: {e}")
first_image = Image.new("RGB", (100, 100), "white")
width, height = first_image.size
# Create a blank white image to hold the grid.
grid_img = Image.new("RGB", (cols * width, rows * height), "white")
total_cells = rows * cols
for idx in range(total_cells):
if idx < len(image_paths):
image_path = image_paths[idx]
try:
img = process_image(image_path, with_text, font_scale)
except Exception as e:
print(f"Error processing image {image_path}: {e}")
img = Image.new("RGB", (width, height), "white")
if img.mode != "RGB":
img = img.convert("RGB")
else:
# If there are not enough images, fill the cell with a white rectangle.
img = Image.new("RGB", (width, height), "white")
# Determine the grid position.
row_idx = idx // cols
col_idx = idx % cols
x_offset = col_idx * width
y_offset = row_idx * height
grid_img.paste(img, (x_offset, y_offset))
return grid_img
def merge_images_to_pdf(images: List[str], output_filename: str,
rows: int, cols: int, with_text: bool, font_scale: float) -> None:
"""
Merge image files into a PDF.
- If grid layout (rows * cols > 1) is specified, images are batched into grid pages.
- If a 1×1 grid is used, each image becomes a separate PDF page.
Images are optionally annotated using ImageMagick if with_text is True.
The annotation includes a white rectangle under the text, and the font size is adjusted by font_scale.
"""
pil_images = []
grid_size = rows * cols
if grid_size == 1:
# Process each image individually.
for image_path in images:
try:
img = process_image(image_path, with_text, font_scale)
except Exception as e:
print(f"Error opening image {image_path}: {e}")
continue
if img.mode != "RGB":
img = img.convert("RGB")
pil_images.append(img)
else:
# Group images in batches and merge each batch into a grid image.
for i in range(0, len(images), grid_size):
batch = images[i:i+grid_size]
grid_img = create_image_grid(batch, rows, cols, with_text, font_scale)
pil_images.append(grid_img)
if pil_images:
try:
# Save the first image and append the rest as additional PDF pages.
pil_images[0].save(output_filename, "PDF", save_all=True, append_images=pil_images[1:])
print(f"PDF saved as '{output_filename}'.")
except Exception as e:
print(f"Error saving PDF: {e}")
else:
print("No images were processed.")
def main() -> None:
"""
Parse command-line arguments and merge images into a PDF.
"""
parser = argparse.ArgumentParser(
description="Merge image files into a single PDF with an optional ImageMagick text annotation (with white undercolor)."
)
parser.add_argument(
"images",
nargs="+",
help="Paths to image files to merge into a PDF."
)
parser.add_argument(
"-o", "--output",
help="Output PDF filename. Defaults to the basename of the first input file with a .pdf extension."
)
parser.add_argument(
"-r", "--rows",
type=int,
default=1,
help="Number of rows in the grid (default: 1)."
)
parser.add_argument(
"-c", "--cols",
type=int,
default=1,
help="Number of columns in the grid (default: 1)."
)
parser.add_argument(
"--with-text",
action="store_true",
help="Annotate images with their filenames using ImageMagick (default is without text)."
)
parser.add_argument(
"--font-scale",
type=float,
default=1.0,
help="Multiplier to adjust the font size for annotations (default: 1.0)."
)
args = parser.parse_args()
# Determine the output filename if not provided.
if args.output:
output_filename = args.output
else:
base_name, _ = os.path.splitext(args.images[0])
output_filename = base_name + ".pdf"
# Confirm before overwriting an existing file.
if os.path.exists(output_filename):
user_input = input(f"File '{output_filename}' already exists. Overwrite? (y/[n]) ").strip().lower()
if user_input != 'y':
print("Aborting.")
sys.exit(0)
merge_images_to_pdf(args.images, output_filename, args.rows, args.cols, args.with_text, args.font_scale)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment