Skip to content

Instantly share code, notes, and snippets.

@thegamecracks
Last active April 17, 2024 16:40
Show Gist options
  • Save thegamecracks/0133b9dbf417d65b3533b27b37b5bd3f to your computer and use it in GitHub Desktop.
Save thegamecracks/0133b9dbf417d65b3533b27b37b5bd3f to your computer and use it in GitHub Desktop.
A basic command-line program for compositing two images using Pillow
"""
Dependencies:
Pillow>=10.3.0
usage: composite.py [-h] [-s SCALE] [-o OFFSET] image
Create a composite of the given image and display it.
The window can be clicked to toggle it as an overlay.
You can also right click it to save the image.
positional arguments:
image The image to be pasted
options:
-h, --help show this help message and exit
-s SCALE, --scale SCALE
Scale image by this factor (default: 1)
-o OFFSET, --offset OFFSET
Offset image by X,Y pixels (default: 0,0)
"""
import argparse
import io
import sys
import textwrap
from tkinter import Event, Label, Menu, PhotoImage, Tk, filedialog
try:
from PIL import Image, ImageOps
except ImportError as e:
sys.exit("Pillow>=10.3.0 must be installed")
BACKGROUND = Image.open("path/to/image.png")
"""The image to use as the background. This dictates the final image size."""
OFFSET = (0, 0)
"""The default center coordinates where the image will be pasted."""
SIZE = (1, 1)
"""The default size of the image in pixels."""
def main() -> None:
description = textwrap.dedent(
"""
Create a composite of the given image and display it.
The window can be clicked to toggle it as an overlay.
You can also right click it to save the image.
"""
)
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-s",
"--scale",
default="1",
help="Scale image by this factor (default: 1)",
type=float,
)
parser.add_argument(
"-o",
"--offset",
default="0,0",
help="Offset image by X,Y pixels (default: 0,0)",
type=parse_offset,
)
parser.add_argument(
"image",
help="The image to be pasted",
type=Image.open,
)
args = parser.parse_args()
scale: float = args.scale
offset: tuple[int, int] = args.offset
image: Image.Image = args.image
image = create_image_composite(image, scale=scale, offset=offset)
enable_windows_dpi_awareness()
app = TkImageShow(image)
app.mainloop()
def parse_offset(s: str) -> tuple[int, int]:
x, _, y = s.partition(",")
x, y = x.strip(), y.strip()
x, y = int(x), int(y)
return x, y
def enable_windows_dpi_awareness() -> None:
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
def create_image_composite(
image: Image.Image,
scale: float = 1,
offset: tuple[int, int] = (0, 0),
) -> Image.Image:
size = (int(SIZE[0] * scale), int(SIZE[1] * scale))
image = ImageOps.contain(image, size, method=Image.Resampling.LANCZOS)
image = image.convert("RGBA")
base = Image.new("RGBA", BACKGROUND.size)
base.paste(BACKGROUND, (0, 0))
pos = (
OFFSET[0] + offset[0] - image.size[0] // 2,
OFFSET[1] + offset[1] - image.size[1] // 2,
)
base.alpha_composite(image, pos)
return base
class TkImageShow(Tk):
def __init__(self, image: Image.Image) -> None:
super().__init__()
self.image = image
self.geometry("{}x{}".format(*image.size))
self.option_add("*tearOff", False)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.label = ImageLabel(self, image=image)
self.label.bind("<1>", self.toggle_overlay)
self.label.grid(sticky="nesw")
self.context = Menu(self)
self.context.add_command(label="Save to file...", command=self.save_to_disk)
self.bind("<3>", lambda event: self.context.post(event.x_root, event.y_root))
def toggle_overlay(self, event: Event) -> None:
not_overlayed = not self.overrideredirect()
self.overrideredirect(not_overlayed)
self.attributes("-topmost", not_overlayed)
def save_to_disk(self) -> None:
file = filedialog.asksaveasfile(
"wb",
filetypes=[("All files", "*")],
initialfile="image.png",
)
if file is None:
return
with file:
self.image.save(file)
class ImageLabel(Label):
def __init__(self, *args, image: Image.Image, **kwargs) -> None:
self.image = image
self._photo = self._create_photo_image(image.size)
self._photo_size = image.size
super().__init__(*args, image=self._photo, **kwargs)
self.bind("<Configure>", self.__on_configure)
def _create_photo_image(self, size: tuple[int, int]) -> PhotoImage:
resized = ImageOps.cover(self.image, size, method=Image.Resampling.BILINEAR)
with io.BytesIO() as f:
resized.save(f, format="ppm")
return PhotoImage(data=f.getvalue())
def __on_configure(self, event: Event) -> None:
window_size = (event.width, event.height)
if self._photo_size != window_size:
self._photo = self._create_photo_image(window_size)
self._photo_size = window_size
self.configure(image=self._photo)
if __name__ == "__main__":
main()
@thegamecracks
Copy link
Author

image-300x372

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment