Skip to content

Instantly share code, notes, and snippets.

@johnjosephhorton
Created July 11, 2025 00:26
Show Gist options
  • Save johnjosephhorton/573c264a9a0e81925cf7e2828c4535ca to your computer and use it in GitHub Desktop.
Save johnjosephhorton/573c264a9a0e81925cf7e2828c4535ca to your computer and use it in GitHub Desktop.
Tool for applying a grid to a PNG and labeling.
#!/usr/bin/env python3
"""
Grid Image Tool
Adds red horizontal and vertical lines to an image and labels each cell with letters.
Dependencies are declared inline for use with uv run.
Usage: uv run grid_image.py input.png --num-vertical 5 --num-horizontal 3
"""
# /// script
# dependencies = [
# "typer",
# "Pillow",
# ]
# ///
import typer
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import string
from typing import Optional
app = typer.Typer()
def generate_labels(num_cells: int) -> list[str]:
"""Generate labels 1, 2, 3, ... for the given number of cells."""
labels = []
for i in range(num_cells):
labels.append(str(i + 1)) # Start from 1 instead of 0
return labels
@app.command()
def main(
image_path: Path = typer.Argument(..., help="Path to the input PNG image"),
num_vertical: int = typer.Option(3, "--num-vertical", help="Number of vertical lines"),
num_horizontal: int = typer.Option(3, "--num-horizontal", help="Number of horizontal lines"),
output_path: Optional[Path] = typer.Option(None, "--output", help="Output path (default: input_grid.png)"),
):
"""Add red grid lines and labels to an image."""
if not image_path.exists():
typer.echo(f"Error: Image file {image_path} not found", err=True)
raise typer.Exit(1)
if output_path is None:
output_path = image_path.parent / f"{image_path.stem}_grid{image_path.suffix}"
try:
with Image.open(str(image_path)) as img:
# Convert to RGB if necessary
if img.mode != 'RGB':
img = img.convert('RGB')
width, height = img.size
draw = ImageDraw.Draw(img)
# Draw vertical lines
if num_vertical > 0:
step_x = width / (num_vertical + 1)
for i in range(1, num_vertical + 1):
x = int(step_x * i)
draw.line([(x, 0), (x, height)], fill='red', width=2)
# Draw horizontal lines
if num_horizontal > 0:
step_y = height / (num_horizontal + 1)
for i in range(1, num_horizontal + 1):
y = int(step_y * i)
draw.line([(0, y), (width, y)], fill='red', width=2)
# Add labels to each cell
cols = num_vertical + 1
rows = num_horizontal + 1
total_cells = cols * rows
labels = generate_labels(total_cells)
cell_width = width / cols
cell_height = height / rows
# Calculate font size based on cell size
font_size = max(24, min(int(cell_width * 0.3), int(cell_height * 0.3)))
# Try to use a default font or fall back to PIL default
font = None
try:
# Try common system fonts
font = ImageFont.truetype("arial.ttf", font_size)
except:
try:
# Try other common fonts on macOS
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", font_size)
except:
try:
# Try Helvetica on macOS
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
except:
# Fall back to PIL default font (bitmap font, fixed size)
font = ImageFont.load_default()
label_idx = 0
for row in range(rows):
for col in range(cols):
if label_idx < len(labels):
x = int(col * cell_width + 5) # 5px padding from left
y = int(row * cell_height + 5) # 5px padding from top
draw.text((x, y), labels[label_idx], fill='black', font=font)
label_idx += 1
# Save the result
img.save(str(output_path))
typer.echo(f"Grid image saved to: {output_path}")
except Exception as e:
typer.echo(f"Error processing image: {e}", err=True)
raise typer.Exit(1)
if __name__ == "__main__":
app()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment