commit d85aa268d30f3f83b87321dea14c31ababc3b6a0 Author: Gregory Marco Date: Thu Jul 10 02:19:32 2025 -0500 Add yagicard tool, based on megacard script. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ed8678 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +*.egg-info +__pycache__ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1e58f32 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "yagicard" +version = "0.0.1" +dependencies = [ + "PyYAML >= 6.0.2", "pillow >= 11.3.0", "goat-tools >= 0.0.1" +] +requires-python = ">=3.11" +authors = [ + {name = "Gregory Marco", email = "greg@nanoyagi.com"} +] +maintainers = [ + {name = "Gregory Marco", email = "greg@nanoyagi.com"} +] + +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project.scripts] +yagicard = "yagicard:main" diff --git a/yagicard/__init__.py b/yagicard/__init__.py new file mode 100755 index 0000000..308b089 --- /dev/null +++ b/yagicard/__init__.py @@ -0,0 +1,215 @@ +import sys +import os +from collections import OrderedDict + +from yaml import load, dump, FullLoader +from PIL import Image, ImageDraw, ImageFont + +from goat import util + +FRAMES_DIRECTORY = "frames" +FONTS_DIRECTORIES = [".", "fonts", os.path.join(os.environ.get("HOME", ""), ".fonts")] + +IMAGES_DIRECTORY = "images" + +DEFAULT_TEXT_COLOR = "black" +DEFAULT_FONT = "EBGaramond-Regular.ttf" +DEFAULT_FONT_SIZE = 12 + +NAME_FONT = "Electrolize-Regular.ttf" +YAGICARDFILE = "Yagicardfile" + +class Field (): + def __init__ (self, style_rules, value, card): + self.style_rules = style_rules + self.card = card + self.value = value + self.value = self.__resolve(style_rules.get("formatter", str)) + + def get (self, rule_name, default_value=None): + return self.__resolve(self.style_rules.get(rule_name, default_value)) + + def has (self, rule_name): + return rule_name in self.style_rules + + def __resolve (self, rule): + try: + return resolve_style_rule(rule, self.value, self.card) + except TypeError: + return resolve_style_rule(rule, self.value) + +def resolve_style_rule (rule, *args): + if callable(rule): + return rule(*args) + + return rule + +def load_font (name, size): + for directory in FONTS_DIRECTORIES: + try: + return ImageFont.truetype(os.path.join(directory, name), size) + except OSError: pass + + raise OSError("failed to open font: " + name) + +def draw_text (image, draw, field): + if not (field.has("y") and field.has("x")): + return + + color = field.get("color", DEFAULT_TEXT_COLOR) + font_size = field.get("size", DEFAULT_FONT_SIZE) + font_name = field.get("typeface", DEFAULT_FONT) + text_anchor = field.get("anchor", None) + alignment = field.get("align", "left") + + font = load_font(font_name, font_size) + font_variant = field.get("font_variant") + if font_variant: + font.set_variation_by_name(font_variant) + + draw.multiline_text((field.get("x"), field.get("y")), field.value, font=font, fill=color, anchor=text_anchor, align=alignment) + +def draw_image (image, draw, field): + source_image = Image.open(os.path.join(IMAGES_DIRECTORY, field.value)) + + width = field.get("width") + height = field.get("height") + + if width and height: + source_image = source_image.resize((width, height)) + + image.paste(source_image, (field.get("x"), field.get("y"))) + +def rescale_text_for_size (threshold, minimum, maximum): + return lambda value: maximum if len(value) < threshold else minimum + +def merge_dicts (dicts): + fusion = {} + for source in dicts: + if not source: + continue + + for key, value in source.items(): + if isinstance(value, dict): + value = merge_dicts([fusion.get(key, None), value]) + fusion[key] = value + return fusion + +def draw_card (card, frame): + image = Image.open(os.path.join(FRAMES_DIRECTORY, frame['image'])) + draw = ImageDraw.Draw(image) + + for field, value in card.items(): + if not field in frame['fields']: + continue + field_frame = resolve_style_rule(frame['fields'][field], value) + field_resolver = Field(field_frame, value, card) + + draw_function = field_frame.get("draw_function", draw_text) + draw_function(image, draw, field_resolver) #field_frame, value, x, y) + return image + +def make_card (card): + return card + +def load_set (set_filename): + card_set = {} + + with open(set_filename) as set_file: + document = load(set_file, Loader=FullLoader) + card_set['name'] = document['name'] + card_set['cards'] = [make_card(card) for card in document['cards']] + + return card_set + +def find_sets (directory): + sets = {} + for entry in os.listdir(directory): + if entry.endswith(".yml"): + sets[os.path.splitext(entry)[0]] = load_set(os.path.join(directory, entry)) + + return sets + +def make_set (card_set, target_directory, default_frame, frames={}): + try: + os.makedirs(target_directory) + except Exception: pass + + card_index = [ + "", + f""" + {card_set['name']}""", + """ + """, + "", + '") + with open(os.path.join(target_directory, "index.html"), "w") as index_html: + index_html.write("\n".join(card_index)) + +def main(): + yagicard_root = util.find_nearest(".", lambda name: name == YAGICARDFILE) + if yagicard_root is None: + return + + yagicardfile = os.path.join(yagicard_root, YAGICARDFILE) + with open(yagicardfile) as yagicardcode: + namespace = { + "YAGICARD_ROOT": yagicard_root + } + namespace.update(globals()) + + exec(yagicardcode.read(), namespace) + frames = namespace.get("frames", {}) + default_frame = namespace['default_frame'] + sets = namespace['sets'] + + for name, setdata in sets.items(): + make_set(setdata, name, default_frame, frames)