Add yagicard tool, based on megacard script.

This commit is contained in:
2025-07-10 02:19:32 -05:00
commit d85aa268d3
3 changed files with 238 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
build
*.egg-info
__pycache__

20
pyproject.toml Normal file
View File

@@ -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"

215
yagicard/__init__.py Executable file
View File

@@ -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 = [
"<html>",
f"""<head>
<title>{card_set['name']}</title>""",
"""<style type="text/css">
div { display: inline-block; max-width: 40%; vertical-align: top }
img { max-width: 100%; }
ul { padding: 0; }
li { list-style-type: none; }
ul.cards > li { display: block; margin-bottom: 15px; }
.label { font-weight: bold; width: 110px; display: inline-block; }
.text { margin-left: 10px; }
.text .image { display: none; }
.text .alignments li:not(:last-child)::after,
.text .subtypes li:not(:last-child)::after { content: ", "; }
.text .alignments li, .text .alignments ul,
.text .subtypes li, .text .subtypes ul{ display: inline; }
</style>
</head>""",
"<body>",
'<ul class="cards">'
]
for i, card in enumerate(card_set['cards']):
image_path = os.path.join(target_directory, f"{i}.png")
frame_name = card.get("frame", card['type'])
frame = frames.get(frame_name, default_frame)
draw_card(card, frame).save(image_path)
card_index.append(f"""
<li>
<div class="image">
<img src="{os.path.basename(image_path)}" />
</div>
<div class="text">
<ul>""")
for key, field in frame['fields'].items():
if not key in card:
continue
html_value = card[key]
if isinstance(html_value, list):
html_value = "<ul>" + "".join([f"<li>{item}</li>" for item in html_value]) + "</ul>"
card_index.append(f"""<li class="{key}">
<span class="label">{key}</span>
<span class="value">{html_value}</span>
</li>""")
card_index.append("""</ul>
</div>
</li>
""")
card_index.append("</ul>")
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)