gifmaker/gifmaker/config.py

458 lines
18 KiB
Python

# Modules
from . import utils
from .argparser import ArgParser
# Standard
import json
import codecs
import textwrap
import random
from argparse import Namespace
from typing import List, Union, Dict, Tuple, Any
from PIL import ImageFont # type: ignore
from pathlib import Path
class Configuration:
# Class to hold all the configuration of the program
# It also interfaces with ArgParser and processes further
def __init__(self) -> None:
self.delay = 700
self.input: Union[Path, None] = None
self.output: Union[Path, None] = None
self.randomfile: Union[Path, None] = None
self.frames: Union[int, None] = None
self.left: Union[int, None] = None
self.right: Union[int, None] = None
self.top: Union[int, None] = None
self.bottom: Union[int, None] = None
self.width: Union[int, None] = None
self.height: Union[int, None] = None
self.words: List[str] = []
self.wordfile: Union[Path, None] = None
self.randomlist: List[str] = []
self.separator = ";"
self.format = "gif"
self.order = "random"
self.font = "sans"
self.fontsize = 60
self.fontcolor: Union[Tuple[int, int, int], str] = (255, 255, 255)
self.bgcolor: Union[Tuple[int, int, int], str, None] = None
self.outline: Union[Tuple[int, int, int], str, None] = None
self.outlinewidth = 2
self.no_outline_left = False
self.no_outline_right = False
self.no_outline_top = False
self.no_outline_bottom = False
self.opacity = 0.66
self.padding = 20
self.radius = 0
self.align = "center"
self.script: Union[Path, None] = None
self.loop = 0
self.remake = False
self.filterlist: List[str] = []
self.filteropts: List[str] = []
self.filter = "none"
self.framelist: List[str] = []
self.frameopts: List[str] = []
self.repeatrandom = False
self.repeatfilter = False
self.fillwords = False
self.fillgen = False
self.nogrow = False
self.wrap = 35
self.nowrap = False
self.verbose = False
self.descender = False
self.seed: Union[int, None] = None
self.frameseed: Union[int, None] = None
self.wordseed: Union[int, None] = None
self.filterseed: Union[int, None] = None
self.colorseed: Union[int, None] = None
self.deepfry = False
self.vertical = False
self.horizontal = False
self.word_color_mode = "normal"
class Internal:
# The path where the main file is located
root: Union[Path, None] = None
# The path where the fonts are located
fontspath: Union[Path, None] = None
# List to keep track of used random words
randwords: List[str] = []
# Counter for [count]
wordcount = 0
# Last font color used
last_fontcolor: Union[Tuple[int, int, int], None] = None
# Random generators
random_frames: Union[random.Random, None] = None
random_words: Union[random.Random, None] = None
random_filters: Union[random.Random, None] = None
random_colors: Union[random.Random, None] = None
# Last word printed
last_words = ""
# Last colors used
last_colors: List[Tuple[int, int, int]] = []
# Response string
response = ""
# Strings for info
rgbstr = "3 numbers from 0 to 255, separated by commas. Names like 'yellow' are also supported"
commastr = "Separated by commas"
# Information about the program
manifest: Dict[str, str]
# Argument definitions
arguments: Dict[str, Any] = {
"input": {"type": str, "help": "Path to a video or image file. Separated by commas"},
"words": {"type": str, "help": "Lines of words to use on the frames"},
"wordfile": {"type": str, "help": "Path of file with word lines"},
"delay": {"type": str, "help": "The delay in ms between frames"},
"left": {"type": int, "help": "Left padding"},
"right": {"type": int, "help": "Right padding"},
"top": {"type": int, "help": "Top padding"},
"bottom": {"type": int, "help": "Bottom padding"},
"width": {"type": int, "help": "Width to resize the frames"},
"height": {"type": int, "help": "Height to resize the frames"},
"frames": {"type": int, "help": "Number of frames to use if no words are provided"},
"output": {"type": str, "help": "Output directory to save the file"},
"format": {"type": str, "choices": ["gif", "webm", "mp4", "jpg", "png"], "help": "The format of the output file"},
"separator": {"type": str, "help": "Character to use as the separator"},
"order": {"type": str, "choices": ["random", "normal"], "help": "The order to use when extracting the frames"},
"font": {"type": str, "help": "The font to use for the text"},
"fontsize": {"type": str, "help": "The size of the font"},
"fontcolor": {"type": str, "help": f"Text color. {rgbstr}"},
"bgcolor": {"type": str, "help": f"Add a background rectangle for the text with this color. {rgbstr}"},
"outline": {"type": str, "help": f"Add an outline around the text with this color. {rgbstr}"},
"outlinewidth": {"type": str, "help": "The width of the outline"},
"opacity": {"type": str, "help": "The opacity of the background rectangle"},
"padding": {"type": str, "help": "The padding of the background rectangle"},
"radius": {"type": str, "help": "The border radius of the background"},
"align": {"type": str, "choices": ["left", "center", "right"], "help": "How to align the center when there are multiple lines"},
"randomlist": {"type": str, "help": "List of words to consider for random words"},
"randomfile": {"type": str, "help": "Path to a list of words to consider for random words"},
"script": {"type": str, "help": "Path to a TOML file that defines the arguments to use"},
"loop": {"type": int, "help": "How to loop a gif render"},
"remake": {"action": "store_true", "help": "Re-render the frames to change the width or delay"},
"filter": {"type": str,
"help": "Color filter to apply to frames",
"choices": ["hue1", "hue2", "hue3", "hue4", "hue5", "hue6", "hue7", "hue8",
"anyhue", "anyhue2", "gray", "grey", "blur", "invert", "random", "random2", "none"]},
"filterlist": {"type": str, "help": f"Filters to use per frame. {commastr}"},
"filteropts": {"type": str, "help": f"The list of allowed filters when picking randomly. {commastr}"},
"framelist": {"type": str, "help": f"List of frame indices to use. {commastr}"},
"frameopts": {"type": str, "help": f"The list of allowed frame indices when picking randomly. {commastr}"},
"repeatrandom": {"action": "store_true", "help": "Repeating random words is ok"},
"repeatfilter": {"action": "store_true", "help": "Repeating random filters is ok"},
"fillwords": {"action": "store_true", "help": "Fill the rest of the frames with the last word line"},
"fillgen": {"action": "store_true", "help": "Generate the first line of words till the end of frames"},
"nogrow": {"action": "store_true", "help": "Don't resize if the frames are going to be bigger than the original"},
"wrap": {"type": str, "help": "Split line if it exceeds this char length"},
"nowrap": {"action": "store_true", "help": "Don't wrap lines"},
"no_outline_left": {"action": "store_true", "help": "Don't draw the left outline"},
"no_outline_right": {"action": "store_true", "help": "Don't draw the right outline"},
"no_outline_top": {"action": "store_true", "help": "Don't draw the top outline"},
"no_outline_bottom": {"action": "store_true", "help": "Don't draw the bottom outline"},
"verbose": {"action": "store_true", "help": "Print more information like time performance"},
"descender": {"action": "store_true", "help": "Apply the height of the descender to the bottom padding of the text"},
"seed": {"type": int, "help": "Seed to use when using any random value"},
"frameseed": {"type": int, "help": "Seed to use when picking frames"},
"wordseed": {"type": int, "help": "Seed to use when picking words"},
"filterseed": {"type": int, "help": "Seed to use when picking filters"},
"colorseed": {"type": int, "help": "Seed to use when picking colors"},
"deepfry": {"action": "store_true", "help": "Compress the frames heavily"},
"vertical": {"action": "store_true", "help": "Append images vertically"},
"horizontal": {"action": "store_true", "help": "Append images horizontally"},
"arguments": {"action": "store_true", "help": "Print argument information"},
"word-color-mode": {"type": str, "choices": ["normal", "random"], "help": "Color mode for words"},
}
aliases = {
"input": ["--i", "-i"],
"output": ["--o", "-o"],
}
def parse_args(self) -> None:
v_title = self.Internal.manifest["title"]
v_version = self.Internal.manifest["version"]
v_info = f"{v_title} {v_version}"
self.Internal.arguments["version"] = {"action": "version",
"help": "Check the version of the program", "version": v_info}
ap = ArgParser(self.Internal.manifest["title"], self.Internal.arguments, self.Internal.aliases, self)
# ---
if getattr(ap.args, "arguments"):
self.Internal.response = self.arguments_json()
return
# ---
ap.path("script")
self.check_script(ap.args)
# ---
string_arg = ap.string_arg()
if string_arg:
ap.args.words = string_arg
# ---
ap.number("fontsize", int)
ap.number("delay", int, duration=True)
ap.number("opacity", float, allow_zero=True)
ap.number("padding", int, allow_zero=True)
ap.number("radius", int, allow_zero=True)
ap.number("outlinewidth", int)
ap.number("wrap", int)
# ---
ap.commas("framelist", int)
ap.commas("frameopts", int)
ap.commas("filterlist", str)
ap.commas("filteropts", str)
ap.commas("fontcolor", int, allow_string=True, is_tuple=True)
ap.commas("bgcolor", int, allow_string=True, is_tuple=True)
ap.commas("outline", int, allow_string=True, is_tuple=True)
# ---
normals = ["left", "right", "top", "bottom", "width", "height", "format", "order",
"font", "frames", "loop", "separator", "filter", "remake", "repeatrandom",
"repeatfilter", "fillwords", "nogrow", "align", "nowrap", "no_outline_left",
"no_outline_right", "no_outline_top", "no_outline_bottom", "verbose", "fillgen",
"descender", "seed", "frameseed", "wordseed", "filterseed", "colorseed",
"deepfry", "vertical", "horizontal", "word_color_mode"]
for normal in normals:
ap.normal(normal)
# ---
paths = ["input", "output", "wordfile", "randomfile"]
for path in paths:
ap.path(path)
# ---
self.fill_paths()
self.check_config(ap.args)
def check_config(self, args: Namespace) -> None:
def separate(value: str) -> List[str]:
return [codecs.decode(utils.clean_lines(item), "unicode-escape")
for item in value.split(self.separator)]
if not self.input:
utils.exit("You need to provide an input file")
return
if not self.output:
utils.exit("You need to provide an output path")
return
if (not self.input.exists()) or (not self.input.is_file()):
utils.exit("Input file does not exist")
return
if self.wordfile:
if not self.wordfile.exists() or not self.wordfile.is_file():
utils.exit("Word file does not exist")
return
self.read_wordfile()
elif args.words:
self.words = separate(args.words)
if args.randomlist:
self.randomlist = separate(args.randomlist)
assert isinstance(self.randomfile, Path)
if not self.randomfile.exists() or not self.randomfile.is_file():
utils.exit("Word file does not exist")
return
if not self.nowrap:
self.wrap_text("words")
if self.vertical or self.horizontal:
if self.format not in ["jpg", "png"]:
self.format = "png"
self.set_random()
def wrap_text(self, attr: str) -> None:
lines = getattr(self, attr)
if not lines:
return
new_lines = []
for line in lines:
lines = line.split("\n")
wrapped = [textwrap.fill(x, self.wrap) for x in lines]
new_lines.append("\n".join(wrapped))
setattr(self, attr, new_lines)
def check_script(self, args: Namespace) -> None:
if self.script is None:
return
data = utils.read_toml(Path(self.script))
if data:
for key in data:
k = ArgParser.dash_to_under(key)
setattr(args, k, data[key])
def read_wordfile(self) -> None:
if self.wordfile:
self.words = self.wordfile.read_text().splitlines()
def fill_root(self, main_file: str) -> None:
self.Internal.root = Path(main_file).parent
self.Internal.fontspath = ArgParser.full_path(Path(self.Internal.root, "fonts"))
def get_manifest(self):
with open(Path(self.Internal.root, "manifest.json"), "r") as file:
self.Internal.manifest = json.load(file)
def fill_paths(self) -> None:
assert isinstance(self.Internal.root, Path)
if not self.randomfile:
self.randomfile = ArgParser.full_path(Path(self.Internal.root, "nouns.txt"))
def get_color(self, attr: str) -> Tuple[int, int, int]:
value = getattr(self, attr)
rgb: Union[Tuple[int, int, int], None] = None
set_config = False
if isinstance(value, str):
if value == "light":
rgb = utils.random_light()
set_config = True
elif value == "light2":
rgb = utils.random_light()
elif value == "dark":
rgb = utils.random_dark()
set_config = True
elif value == "dark2":
rgb = utils.random_dark()
elif (value == "font") and isinstance(self.Internal.last_fontcolor, tuple):
rgb = self.Internal.last_fontcolor
elif value == "lightfont" and isinstance(self.Internal.last_fontcolor, tuple):
rgb = utils.light_contrast(self.Internal.last_fontcolor)
set_config = True
elif value == "lightfont2" and isinstance(self.Internal.last_fontcolor, tuple):
rgb = utils.light_contrast(self.Internal.last_fontcolor)
elif value == "darkfont" and isinstance(self.Internal.last_fontcolor, tuple):
rgb = utils.dark_contrast(self.Internal.last_fontcolor)
set_config = True
elif value == "darkfont2" and isinstance(self.Internal.last_fontcolor, tuple):
rgb = utils.dark_contrast(self.Internal.last_fontcolor)
else:
rgb = utils.color_name(value)
elif isinstance(value, (list, tuple)) and len(value) >= 3:
rgb = (value[0], value[1], value[2])
ans = rgb or (100, 100, 100)
if attr == "fontcolor":
self.Internal.last_fontcolor = ans
if set_config:
setattr(self, attr, rgb)
return ans
def set_random(self) -> None:
def set_rng(attr: str, rng_name: str) -> None:
value = getattr(self, attr)
if value is not None:
rand = random.Random(value)
elif self.seed is not None:
rand = random.Random(self.seed)
else:
rand = random.Random()
setattr(self.Internal, rng_name, rand)
set_rng("frameseed", "random_frames")
set_rng("wordseed", "random_words")
set_rng("filterseed", "random_filters")
set_rng("colorseed", "random_colors")
def get_font(self) -> ImageFont.FreeTypeFont:
fonts = {
"sans": "Roboto-Regular.ttf",
"serif": "RobotoSerif-Regular.ttf",
"mono": "RobotoMono-Regular.ttf",
"italic": "Roboto-Italic.ttf",
"bold": "Roboto-Bold.ttf",
"cursive": "Pacifico-Regular.ttf",
"comic": "ComicNeue-Regular.ttf",
"nova": "NovaSquare-Regular.ttf",
}
def random_font() -> str:
return random.choice(list(fonts.keys()))
if self.font == "random":
font = random_font()
font_file = fonts[font]
self.font = font
elif self.font == "random2":
font = random_font()
font_file = fonts[font]
elif ".ttf" in self.font:
font_file = str(ArgParser.resolve_path(Path(self.font)))
elif self.font in fonts:
font_file = fonts[self.font]
else:
font_file = fonts["sans"]
assert isinstance(self.Internal.fontspath, Path)
path = Path(self.Internal.fontspath, font_file)
return ImageFont.truetype(path, size=self.fontsize)
def arguments_json(self) -> str:
filter_out = ["string_arg", "arguments"]
new_dict = {
key: {k: v for k, v in value.items() if (k != "type" and k != "action")}
for key, value in self.Internal.arguments.items() if key not in filter_out
}
for key in new_dict:
if hasattr(self, key):
new_dict[key]["value"] = getattr(self, key)
return json.dumps(new_dict)
# Main configuration object
config = Configuration()