Skip to content

Commit

Permalink
Merge pull request #7 from TylerLubeck/image-generator
Browse files Browse the repository at this point in the history
GIF Generator Utility
  • Loading branch information
TylerLubeck authored Aug 12, 2020
2 parents 0fd7a31 + d7e6cf1 commit 2363a9d
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 3 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
examples/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM python:3.8
COPY . /app
WORKDIR /app
RUN pip install .
ENTRYPOINT ["battlebots"]
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ configured to produce a docker image that follows all of the rules and conventio
So long as you also follow the rules and conventions described in this document, you're free to submit any docker
image you like - there's no need to start with any of the provided examples.

# Submitting Your Bot
## Submitting Your Bot

All bots must be submitted as runnable docker images. For a submitted image `IMAGE`, it will be run as
```bash
Expand All @@ -157,9 +157,26 @@ docker run \
**Note:** `network=none` means that your code will not be able to access any form of network or internet at runtime.
This is to prevent people from using external resources to track other bot performance.

Docker images must be published to Docker Hub. You can create an account at https://hub.docker.com/signup
Docker images must be published to either Docker Hub or Github Packages. See me for more help if you need it.

We're using Docker Hub over any internal image repositories for security, cost, and ease-of-use reasons.
We're using external docker repositories over any internal image repositories for security, cost, and ease-of-use reasons.

## Testing ASCII Art

To speed up ASCII Art testing, there's a utility built in to the battlebot runner to generate GIFs from ascii art files

Create a folder and fill it with files representing the frames of your gif. These files must end in `.ascii` and will
be loaded in alphabetical order to create frames.

You can then create a gif as such:

```bash
IMAGE_PATH=/path/to/ascii/folder
docker run \
-v ${IMAGE_PATH}:/images \
docker.pkg.github.com/tylerlubeck/battlebots/battlebots:latest \
generate-gif /images /images/output.gif
```

# The Tournament

Expand Down
37 changes: 37 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from setuptools import setup


install_requires = [
'colorama>=0.4.3,<=0.5.0',
'docker>=4.2.2,<5.0.0',
'typer>=0.3.1,<0.4.0',
'pillow>=7.2.0<8.0.0',
]

entry_points = {
'console_scripts': [
'battlebots = battlebots.__main__:main'
]
}

setup_kwargs = {
'name': 'battlebots',
'version': '0.1.0',
'description': 'BagelCon Battlebots Runner',
'long_description': 'The Battlebots runner',
'author': 'Tyler Lubeck',
'author_email': '[email protected]',
'maintainer': None,
'maintainer_email': None,
'url': '',
'package_dir': {'': 'src'},
'packages': ['battlebots'],
'package_data': {'': ['*']},
'install_requires': install_requires,
'entry_points': entry_points,
'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
Empty file added src/battlebots/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions src/battlebots/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from battlebots.cli import main

# main is used as the entrypoint here

if __name__ == "__main__":
main()
105 changes: 105 additions & 0 deletions src/battlebots/art_to_gif.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import logging
from pathlib import Path # For Typing
import string
from typing import Any
from typing import BinaryIO
from typing import List
from typing import Union

import PIL
import PIL.Image
import PIL.ImageFont
import PIL.ImageDraw

PIXEL_ON = 0
PIXEL_OFF = 255

logger = logging.getLogger(__name__)

def _point_to_pixel(point: int) -> int:
"""IDK but it was in the example so"""
return int(round(point * 96.0 / 72))


def _get_font(font_path='cour.ttf', font_size=20) -> PIL.ImageFont.ImageFont:
"""Load the specified font, or fallback to the system default
Args:
font_path: The path to the truetype font file to load
font_size: The size of the font to load
Returns:
A loaded ImageFont file
Notes:
If the specified font_path is not available, we load the system default font
"""
try:
font = PIL.ImageFont.truetype(font_path, size=font_size)
except IOError:
font = PIL.ImageFont.load_default()
logger.info("Failed to load font, using default", exc_info=True)

return font


def _get_size(lines: List[str], font: PIL.ImageFont.ImageFont) -> (int, int, int):
"""Determine image width, height, and line spacing
Args:
lines: The lines that will make up the ASCII Image
font: The font to be used
Returns:
(width, height, line_spacing)
"""
max_width_line = max(lines, key=lambda s: font.getsize(s)[0])
test_string = string.ascii_uppercase
max_height = _point_to_pixel(font.getsize(string.ascii_uppercase)[1])
max_width = _point_to_pixel(font.getsize(max_width_line)[0])

height = max_height * len(lines)
width = int(round(max_width + 10)) # Little padding makes it nicer to see
line_spacing = int(round(max_height * 0.8))

return width, height, line_spacing


def create_frame(art: List[str]) -> PIL.Image.Image:
"""Create an image from the ascii art array
Args:
art: A list of strings comprising an ASCII art image
Returns:
An image object with the rendered ASCII art
"""
lines = [l.rstrip() for l in art] # We don't need trailing whitespace on the right hand side

font = _get_font()
width, height, line_spacing = _get_size(lines, font)

# 'P' means palette image
image = PIL.Image.new('P', (width, height), color=PIXEL_OFF)
draw = PIL.ImageDraw.Draw(image)

vertical_position = 5 # Start 5 pixels from the top
horizontal_position = 5 # Always start 5 pixels from the left
for line in lines:
position = (horizontal_position, vertical_position)
draw.text(position, line, fill=PIXEL_ON, font=font)
vertical_position += line_spacing

image = image.crop(image.getbbox())
return image


def frames_to_gif(filename: Union[str, Path, BinaryIO], frames: List[PIL.Image.Image], frame_length=150):
"""Generate a gif file from the supplied frames
Args:
filename: The file path, or file pointer, to save the file to.
frames: A list of PIL images to include in the gif
frame_length: The amount of time, in ms, that a frame is displayed for
"""
frames[0].save(filename, save_all=True, append_images=frames[1:], optimize=False, duration=frame_length, loop=0)
46 changes: 46 additions & 0 deletions src/battlebots/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
from pathlib import Path

import typer

from battlebots.art_to_gif import create_frame, frames_to_gif

main = typer.Typer()

@main.command()
def generate_gif(
directory: Path = typer.Argument(..., help="The directory to scan for ascii art"),
output: Path = typer.Argument(..., help="The name of the file the gif should be written to"),
frame_length: int = typer.Option(default=150, help="The length of a frame, in ms"),
):
"""Generate an animated GIF from a series of ascii art files
The files in DIRECTORY will be alphabetized, turned in to images, and smashed together to create an animated GIF
The resulting gif will be written to OUTPUT
"""
file_names = []
typer.secho("Loading Image Files...")
with os.scandir(directory) as it, typer.progressbar(it) as progress:
for entry in progress:
if entry.is_file() and entry.name.endswith('.ascii'):
file_names.append(entry.path)

file_names.sort()

typer.secho("Generating Frames...")
frames = []
with typer.progressbar(file_names) as progress:
for fname in progress:
with open(fname, 'r') as f:
frames.append(create_frame(f.readlines()))

typer.secho("Writing GIF...")
frames_to_gif(output, frames, frame_length)

typer.secho("done", bold=True, fg="green")

@main.callback()
def callback():
"""Placeholder"""
pass

0 comments on commit 2363a9d

Please sign in to comment.