Add yagicard tool, based on megacard script.
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build
|
||||
*.egg-info
|
||||
__pycache__
|
||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal 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
215
yagicard/__init__.py
Executable 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)
|
||||
Reference in New Issue
Block a user