Skip to content

Instantly share code, notes, and snippets.

@larrasket
Created February 25, 2026 23:41
Show Gist options
  • Select an option

  • Save larrasket/2668625ddf7fe9be2a86303ad71640a7 to your computer and use it in GitHub Desktop.

Select an option

Save larrasket/2668625ddf7fe9be2a86303ad71640a7 to your computer and use it in GitHub Desktop.
Processes images for macOS "Fit to Screen" wallpaper mode so image content is GUARANTEED to never appear under the menu bar or dock.
#!/usr/bin/env python3
"""
process_wallpapers.py
Processes images for macOS "Fit to Screen" wallpaper mode so image content
is GUARANTEED to never appear under the menu bar or dock.
Strategy
--------
Every output image is a full screen-sized canvas (screen_width × screen_height).
The image content is scaled to fit inside a safe inner rectangle:
inner width = screen_width
inner height = screen_height - top_padding - bottom_padding
…while preserving the original aspect ratio. The scaled image is then centered
inside that inner rectangle, and the whole canvas (with black bars top and
bottom) becomes the wallpaper file.
When macOS scales this canvas with "Fit to Screen" it maps it 1-to-1 to the
display. The black bars sit exactly over the menu bar and dock regions —
image content is guaranteed to stay inside the safe zone.
Originals are never modified. All output goes to a separate folder.
Usage
-----
python3 process_wallpapers.py [OPTIONS] INPUT_FOLDER OUTPUT_FOLDER
Options
-------
--screen-width INT Screen width in points/pixels (default: 2560)
--screen-height INT Screen height in points/pixels (default: 1600)
--top-padding INT Minimum gap above image content (default: 100)
--bottom-padding INT Minimum gap below image content (default: 100)
--bg-color STR Canvas colour as R,G,B (default: 0,0,0)
--suffix STR Suffix added to output filenames(default: _padded)
--help Show this help and exit
"""
import argparse
import sys
from pathlib import Path
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".bmp", ".webp", ".heic"}
# ──────────────────────────────────────────────────────────────────────────────
# CLI
# ──────────────────────────────────────────────────────────────────────────────
def parse_args():
parser = argparse.ArgumentParser(
description="Prepare images for macOS 'Fit to Screen' wallpaper with guaranteed safe-zone padding.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("input_folder", type=Path, help="Folder containing source images")
parser.add_argument("output_folder", type=Path, help="Folder for processed images")
parser.add_argument("--screen-width", type=int, default=2560, help="Screen width (px, default 2560)")
parser.add_argument("--screen-height", type=int, default=1600, help="Screen height (px, default 1600)")
parser.add_argument("--top-padding", type=int, default=100, help="Min pixels above content (default 100)")
parser.add_argument("--bottom-padding", type=int, default=100, help="Min pixels below content (default 100)")
parser.add_argument("--bg-color", type=str, default="0,0,0",
help="Canvas background colour as R,G,B (default: 0,0,0 = black)")
parser.add_argument("--suffix", type=str, default="_padded",
help="Suffix appended before extension on output files (default: _padded)")
return parser.parse_args()
def parse_color(raw: str):
try:
parts = [int(x.strip()) for x in raw.split(",")]
if len(parts) != 3 or not all(0 <= p <= 255 for p in parts):
raise ValueError
return tuple(parts)
except ValueError:
print(f"ERROR: --bg-color must be three integers 0-255 separated by commas, e.g. '0,0,0'. Got: {raw!r}")
sys.exit(1)
# ──────────────────────────────────────────────────────────────────────────────
# Pillow bootstrap
# ──────────────────────────────────────────────────────────────────────────────
def ensure_pillow():
try:
from PIL import Image # noqa: F401
except ImportError:
print("Pillow is not installed. Installing now …")
import subprocess
subprocess.check_call([sys.executable, "-m", "pip", "install", "Pillow", "--quiet"])
print("Pillow installed successfully.\n")
# ──────────────────────────────────────────────────────────────────────────────
# Core processing
# ──────────────────────────────────────────────────────────────────────────────
def process_image(
src_path: Path,
output_folder: Path,
screen_w: int,
screen_h: int,
top_pad: int,
bot_pad: int,
bg_color: tuple,
suffix: str,
) -> Path:
from PIL import Image, ImageOps
img = Image.open(src_path)
# Honour EXIF rotation so portrait shots are not treated as landscape.
try:
img = ImageOps.exif_transpose(img)
except Exception:
pass
orig_w, orig_h = img.size
orig_mode = img.mode
# ── Safe inner rectangle ──────────────────────────────────────────────────
# Maximum pixel space available for image content after reserving margins.
inner_w = screen_w
inner_h = screen_h - top_pad - bot_pad
if inner_h <= 0:
raise ValueError(
f"top_padding ({top_pad}) + bottom_padding ({bot_pad}) "
f">= screen_height ({screen_h}). No room for image content."
)
# ── Scale image to fit inside the inner rectangle (box-fit) ──────────────
# Constrain both width and height; preserve aspect ratio.
scale = min(inner_w / orig_w, inner_h / orig_h)
if scale < 1.0:
fit_w = max(1, round(orig_w * scale))
fit_h = max(1, round(orig_h * scale))
img = img.resize((fit_w, fit_h), Image.LANCZOS)
action = f"scaled down {orig_w}×{orig_h} → {fit_w}×{fit_h} (factor {scale:.4f})"
else:
# Image already fits — keep its native size; it will be centred with
# extra padding on all sides. No upscaling.
fit_w, fit_h = orig_w, orig_h
action = f"kept original {fit_w}×{fit_h} (fits within safe zone)"
# ── Build full-screen canvas ──────────────────────────────────────────────
# The canvas is exactly screen_w × screen_h. When "Fit to Screen" maps this
# 1-to-1 onto the display, the padding regions land precisely over the
# menu bar and dock — image content is provably inside the safe zone.
canvas_mode = "RGBA" if orig_mode == "RGBA" else "RGB"
bg = (bg_color[0], bg_color[1], bg_color[2], 255) if canvas_mode == "RGBA" else bg_color
canvas = Image.new(canvas_mode, (screen_w, screen_h), bg)
# Centre horizontally on the screen; centre vertically within the safe zone.
paste_x = (screen_w - fit_w) // 2
paste_y = top_pad + (inner_h - fit_h) // 2
actual_top = paste_y # guaranteed >= top_pad
actual_bot = screen_h - paste_y - fit_h # guaranteed >= bot_pad
# Flatten RGBA onto background before pasting into RGB canvas.
if img.mode == "RGBA" and canvas_mode == "RGB":
bg_layer = Image.new("RGBA", img.size, bg)
bg_layer.paste(img, mask=img.split()[3])
img = bg_layer.convert("RGB")
canvas.paste(img, (paste_x, paste_y))
# ── Save ──────────────────────────────────────────────────────────────────
ext = src_path.suffix.lower()
out_name = src_path.stem + suffix + src_path.suffix
out_path = output_folder / out_name
save_kwargs: dict = {}
if ext in (".jpg", ".jpeg"):
if canvas.mode == "RGBA":
canvas = canvas.convert("RGB")
save_kwargs = {"quality": 95, "subsampling": 0}
elif ext == ".png":
save_kwargs = {"optimize": True}
canvas.save(out_path, **save_kwargs)
# ── Console report ────────────────────────────────────────────────────────
print(f" {action}")
print(f" Canvas : {screen_w} × {screen_h} px (full screen)")
print(f" Image pasted : ({paste_x}, {paste_y}) → {fit_w}×{fit_h}")
print(f" Actual padding: top={actual_top}px bottom={actual_bot}px "
f"left/right={(screen_w - fit_w) // 2}px")
print(f" → Saved: {out_name}")
return out_path
# ──────────────────────────────────────────────────────────────────────────────
# Entry point
# ──────────────────────────────────────────────────────────────────────────────
def main():
args = parse_args()
bg_color = parse_color(args.bg_color)
if not args.input_folder.is_dir():
print(f"ERROR: Input folder not found: {args.input_folder}")
sys.exit(1)
args.output_folder.mkdir(parents=True, exist_ok=True)
safe_h = args.screen_height - args.top_padding - args.bottom_padding
print("=" * 66)
print(" macOS Wallpaper Safe-Zone Processor")
print("=" * 66)
print(f" Screen : {args.screen_width} × {args.screen_height} px")
print(f" Top padding : {args.top_padding} px ← always clear of menu bar")
print(f" Bottom padding : {args.bottom_padding} px ← always clear of dock")
print(f" Max content h : {safe_h} px")
print(f" Output canvas : {args.screen_width} × {args.screen_height} px (full screen)")
print(f" BG colour : RGB{bg_color}")
print(f" Output suffix : '{args.suffix}'")
print(f" Input folder : {args.input_folder.resolve()}")
print(f" Output folder : {args.output_folder.resolve()}")
print("=" * 66)
print()
print(" NOTE: EVERY image receives a full-screen canvas with padding,")
print(" no matter its original size. No image is ever skipped.")
print()
ensure_pillow()
images = sorted(
p for p in args.input_folder.iterdir()
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
)
if not images:
print(f"No supported images found in: {args.input_folder}")
print(f"Supported formats: {', '.join(sorted(SUPPORTED_EXTENSIONS))}")
sys.exit(0)
print(f"Found {len(images)} image(s) to process.\n")
ok = failed = 0
for i, src in enumerate(images, 1):
print(f"[{i}/{len(images)}] {src.name}")
try:
process_image(
src_path=src,
output_folder=args.output_folder,
screen_w=args.screen_width,
screen_h=args.screen_height,
top_pad=args.top_padding,
bot_pad=args.bottom_padding,
bg_color=bg_color,
suffix=args.suffix,
)
ok += 1
except Exception as exc:
print(f" FAILED: {exc}")
failed += 1
print()
print("=" * 66)
print(f" Done — {ok} succeeded, {failed} failed.")
print(f" Output: {args.output_folder.resolve()}")
print("=" * 66)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment