Rewrite for SVG support
heros and environments only at the moment
This commit is contained in:
parent
810369596d
commit
78ac4d1da3
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
||||||
/__pycache__/
|
/__pycache__/
|
||||||
/build/
|
/build/
|
||||||
/dist/
|
/dist/
|
||||||
|
/out/**
|
||||||
|
|
|
@ -1,95 +1,100 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
#note: image templates: https://boardgamegeek.com/thread/813176/card-templates
|
|
||||||
|
|
||||||
#TODO: dynamic text sizes
|
|
||||||
#TODO: remote upload?
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
import textwrap
|
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
if getattr(sys, 'frozen', False): # we are running in a bundle
|
if getattr(sys, 'frozen', False): # we are running in a bundle
|
||||||
bundle_dir = sys._MEIPASS
|
bundle_dir = sys._MEIPASS
|
||||||
else: # we are running in a normal Python environment
|
else: # we are running in a normal Python environment
|
||||||
bundle_dir = os.path.dirname(os.path.abspath(__file__))
|
bundle_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
IMG_HERO_CHAR_FRONT = Image.open(bundle_dir + "/images/HeroCharFront.png")
|
CARD_WIDTH = 181 * 4
|
||||||
IMG_HERO_CHAR_BACK = Image.open(bundle_dir + "/images/HeroCharBack.png")
|
CARD_HEIGHT = 253 * 4
|
||||||
IMG_HERO_DECK = Image.open(bundle_dir + "/images/HeroCard.png")
|
|
||||||
IMG_TARGET_HP = Image.open(bundle_dir + "/images/targetHP.png")
|
|
||||||
|
|
||||||
FONT_KEYWORD = ImageFont.truetype(bundle_dir + "/fonts/RedStateBlueStateBB_reg.otf", size=40)
|
def setText(tree, id, text):
|
||||||
FONT_TITLE = ImageFont.truetype(bundle_dir + "/fonts/CrashLandingBB.otf", size=60)
|
element = tree.find('.//*[@id="' + id + '"]')
|
||||||
FONT_DESCRIPTION = ImageFont.truetype(bundle_dir + "/fonts/RedStateBlueStateBB_reg.otf", size=30)
|
if element is None:
|
||||||
FONT_TARGET_HP = ImageFont.truetype(bundle_dir + "/fonts/CrashLandingBB.otf", size=120)
|
print("id", id, "not found")
|
||||||
FONT_CHAR_HP = ImageFont.truetype(bundle_dir + "/fonts/ap.ttf", size=120)
|
return
|
||||||
|
elif element.tag == "{http://www.w3.org/2000/svg}flowRoot":
|
||||||
|
element.find("{http://www.w3.org/2000/svg}flowPara").text = text
|
||||||
|
else:
|
||||||
|
element[0].text = str(text)
|
||||||
|
|
||||||
def drawTextIf(draw, position, font, card, key, wrap=False):
|
def removeElement(tree, id):
|
||||||
if key in card:
|
mark = tree.find('.//*[@id="' + id + '"]')
|
||||||
text = str(card[key])
|
if mark is not None:
|
||||||
|
mark.getparent().remove(mark)
|
||||||
|
|
||||||
if wrap:
|
def makeSVG(base, properties):
|
||||||
# badly wrap text to fit on card
|
tree = etree.parse(base)
|
||||||
text = '\n'.join(textwrap.fill(line.strip(), wrap,
|
for id, text in properties.items():
|
||||||
break_long_words=False,
|
if id not in ["count", "art"]:
|
||||||
replace_whitespace=False)
|
setText(tree, id, text)
|
||||||
for line in text.splitlines())
|
|
||||||
draw.text(position, text, font=font, fill="#000000")
|
|
||||||
|
|
||||||
def addCardToBase(baseImg, baseX, cardNum, cardImg):
|
# remove HP mark if there is no hp
|
||||||
|
if "hp" not in properties:
|
||||||
|
removeElement(tree, "hpMark")
|
||||||
|
|
||||||
|
if "art" in properties:
|
||||||
|
art = tree.find('.//*[@id="art"]')
|
||||||
|
if art is not None:
|
||||||
|
art.set("{http://www.w3.org/1999/xlink}href",
|
||||||
|
"file://" + bundle_dir + "/" + properties["art"])
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def addCardToBase(svg, baseImg, baseX, cardNum):
|
||||||
|
# TODO: possible to remove all this writing to temp files?
|
||||||
|
dest = "out/fig" + str(cardNum)
|
||||||
|
svg.write(dest + ".svg")
|
||||||
|
subprocess.call(["inkscape", "-z", "-f", dest + ".svg", "-w",
|
||||||
|
str(CARD_WIDTH), "-e", dest + ".png"])
|
||||||
|
cardImg = Image.open(dest + ".png")
|
||||||
baseImg.paste(cardImg, ((cardNum % baseX) * cardImg.width,
|
baseImg.paste(cardImg, ((cardNum % baseX) * cardImg.width,
|
||||||
int(cardNum / baseX) * cardImg.height))
|
int(cardNum / baseX) * cardImg.height))
|
||||||
|
|
||||||
|
def makeFace(baseImage, baseX, cardNum, base, card):
|
||||||
|
fig = makeSVG(base, card)
|
||||||
|
addCardToBase(fig, baseImage, baseX, cardNum)
|
||||||
|
|
||||||
def makeFaces(deckJson, outfile):
|
def makeFaces(deckJson, outfile):
|
||||||
# TODO: probably a more optimal way to fit cards
|
baseX = math.ceil(math.sqrt(len(deckJson['deck']) + len(deckJson['character']) * 2))
|
||||||
baseX = math.ceil(math.sqrt(len(deckJson['deck']) +
|
baseImage = Image.new('RGB', (CARD_WIDTH * baseX,
|
||||||
len(deckJson['character']) * 2))
|
CARD_HEIGHT * baseX))
|
||||||
baseImage = Image.new('RGB', (IMG_HERO_DECK.width * baseX,
|
|
||||||
IMG_HERO_DECK.height * baseX))
|
cardType = deckJson["type"]
|
||||||
|
|
||||||
cardNum = 0
|
cardNum = 0
|
||||||
# Make a card for each hero character card
|
# Make a card for each character card
|
||||||
|
if cardType == "hero":
|
||||||
for card in deckJson['character']:
|
for card in deckJson['character']:
|
||||||
cardFrontImg = IMG_HERO_CHAR_FRONT.copy()
|
makeFace(baseImage, baseX, cardNum,
|
||||||
draw = ImageDraw.Draw(cardFrontImg)
|
os.path.join("images", cardType, "charFront.svg"), card)
|
||||||
drawTextIf(draw, (150, 70), FONT_CHAR_HP, card, 'name')
|
|
||||||
drawTextIf(draw, (100, 900), FONT_DESCRIPTION, card, 'power', 40)
|
|
||||||
drawTextIf(draw, (53, 260), FONT_CHAR_HP, card, 'hp')
|
|
||||||
addCardToBase(baseImage, baseX, cardNum, cardFrontImg)
|
|
||||||
cardNum += 1
|
cardNum += 1
|
||||||
|
|
||||||
cardBackImg = IMG_HERO_CHAR_BACK.copy()
|
makeFace(baseImage, baseX, cardNum,
|
||||||
draw = ImageDraw.Draw(cardBackImg)
|
os.path.join("images", cardType, "charBack.svg"), card)
|
||||||
drawTextIf(draw, (150, 70), FONT_CHAR_HP, card, 'name')
|
|
||||||
drawTextIf(draw, (80, 800), FONT_DESCRIPTION, card, 'incapacitated', 43)
|
|
||||||
addCardToBase(baseImage, baseX, cardNum, cardBackImg)
|
|
||||||
cardNum += 1
|
cardNum += 1
|
||||||
|
|
||||||
# Make a card for each card in the deck
|
if cardType == "villain":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Make a card for each card
|
||||||
for card in deckJson['deck']:
|
for card in deckJson['deck']:
|
||||||
cardImg = IMG_HERO_DECK.copy()
|
makeFace(baseImage, baseX, cardNum,
|
||||||
|
os.path.join("images", cardType, "card.svg"), card)
|
||||||
if 'hp' in card: # Draw HP marker, if it exists
|
|
||||||
# self mask for transparency
|
|
||||||
cardImg.paste(IMG_TARGET_HP, (510, 36), IMG_TARGET_HP)
|
|
||||||
|
|
||||||
draw = ImageDraw.Draw(cardImg)
|
|
||||||
drawTextIf(draw, (52, 50), FONT_TITLE, card, 'name')
|
|
||||||
drawTextIf(draw, (85, 610), FONT_KEYWORD, card, 'type')
|
|
||||||
drawTextIf(draw, (52, 670), FONT_DESCRIPTION, card, 'description', 46)
|
|
||||||
drawTextIf(draw, (120, 910), FONT_DESCRIPTION, card, 'flavor')
|
|
||||||
drawTextIf(draw, (600, 40), FONT_TARGET_HP, card, 'hp')
|
|
||||||
addCardToBase(baseImage, baseX, cardNum, cardImg)
|
|
||||||
cardNum += 1
|
cardNum += 1
|
||||||
|
|
||||||
baseImage.save(outfile + ".png", "PNG")
|
baseImage.save(outfile + ".png", "PNG")
|
||||||
return (baseX, baseX)
|
return baseX
|
||||||
|
|
||||||
def makeJson(deckJson, imgWidth, imgHeight, outfile):
|
def makeJson(deckJson, imgWidth, outfile):
|
||||||
with open(bundle_dir + "/templates/deck.json") as f:
|
with open(bundle_dir + "/templates/deck.json") as f:
|
||||||
outJson = json.load(f)
|
outJson = json.load(f)
|
||||||
with open(bundle_dir + "/templates/card.json") as f:
|
with open(bundle_dir + "/templates/card.json") as f:
|
||||||
|
@ -98,7 +103,7 @@ def makeJson(deckJson, imgWidth, imgHeight, outfile):
|
||||||
# number of cards in x and y direction
|
# number of cards in x and y direction
|
||||||
outJson['ObjectStates'][0]['CustomDeck']['1'].update(
|
outJson['ObjectStates'][0]['CustomDeck']['1'].update(
|
||||||
{"NumWidth": imgWidth,
|
{"NumWidth": imgWidth,
|
||||||
"NumHeight": imgHeight,
|
"NumHeight": imgWidth,
|
||||||
#"FaceURL": "http://adam-desktop.dyn.wpi.edu:8000/" + outfile + ".png",
|
#"FaceURL": "http://adam-desktop.dyn.wpi.edu:8000/" + outfile + ".png",
|
||||||
"FaceURL": "file://" + os.getcwd() + "/" + outfile + ".png",
|
"FaceURL": "file://" + os.getcwd() + "/" + outfile + ".png",
|
||||||
"BackURL": "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"})
|
"BackURL": "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"})
|
||||||
|
@ -148,15 +153,5 @@ if __name__ == '__main__':
|
||||||
with open(inputJson) as f:
|
with open(inputJson) as f:
|
||||||
deckJson = json.load(f)
|
deckJson = json.load(f)
|
||||||
|
|
||||||
imgWidth, imgHeight = makeFaces(deckJson, outfile)
|
imgWidth = makeFaces(deckJson, outfile)
|
||||||
makeJson(deckJson, imgWidth, imgHeight, outfile)
|
makeJson(deckJson, imgWidth, outfile)
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
with open("hero_3.json") as f:
|
|
||||||
deckJson = json.load(f)
|
|
||||||
|
|
||||||
outfile = "out"
|
|
||||||
|
|
||||||
imgWidth, imgHeight = makeFaces(deckJson, outfile)
|
|
||||||
makeJson(deckJson, imgWidth, imgHeight, outfile)
|
|
||||||
|
|
1918
images/environment/card.svg
Normal file
1918
images/environment/card.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 409 KiB |
2599
images/hero/card.svg
Normal file
2599
images/hero/card.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 492 KiB |
9211
images/hero/charBack.svg
Normal file
9211
images/hero/charBack.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.5 MiB |
9345
images/hero/charFront.svg
Normal file
9345
images/hero/charFront.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.5 MiB |
4021
images/source/Environment Deck.ai
Normal file
4021
images/source/Environment Deck.ai
Normal file
File diff suppressed because one or more lines are too long
5483
images/source/Hero Deck.ai
Normal file
5483
images/source/Hero Deck.ai
Normal file
File diff suppressed because one or more lines are too long
5095
images/source/Villain Deck.ai
Normal file
5095
images/source/Villain Deck.ai
Normal file
File diff suppressed because one or more lines are too long
29
templates/card.json
Normal file
29
templates/card.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"Name": "Card",
|
||||||
|
"Transform": {
|
||||||
|
"posX": 0,
|
||||||
|
"posY": 0,
|
||||||
|
"posZ": 0,
|
||||||
|
"rotX": 0,
|
||||||
|
"rotY": 0,
|
||||||
|
"rotZ": 0,
|
||||||
|
"scaleX": 1.0,
|
||||||
|
"scaleY": 1.0,
|
||||||
|
"scaleZ": 1.0
|
||||||
|
},
|
||||||
|
"Nickname": "",
|
||||||
|
"Description": "",
|
||||||
|
"ColorDiffuse": {
|
||||||
|
"r": 0.713235259,
|
||||||
|
"g": 0.713235259,
|
||||||
|
"b": 0.713235259
|
||||||
|
},
|
||||||
|
"Locked": false,
|
||||||
|
"Grid": true,
|
||||||
|
"Snap": true,
|
||||||
|
"Autoraise": true,
|
||||||
|
"Sticky": true,
|
||||||
|
"Tooltip": true,
|
||||||
|
"CardID": 0,
|
||||||
|
"SidewaysCard": false
|
||||||
|
}
|
44
templates/deck.json
Normal file
44
templates/deck.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"SaveName": "",
|
||||||
|
"GameMode": "",
|
||||||
|
"Date": "",
|
||||||
|
"Table": "",
|
||||||
|
"Sky": "",
|
||||||
|
"Note": "",
|
||||||
|
"Rules": "",
|
||||||
|
"PlayerTurn": "",
|
||||||
|
"ObjectStates": [
|
||||||
|
{
|
||||||
|
"Name": "DeckCustom",
|
||||||
|
"Transform": {
|
||||||
|
"posX": 0,
|
||||||
|
"posY": 0,
|
||||||
|
"posZ": 0,
|
||||||
|
"rotX": 0,
|
||||||
|
"rotY": 0.0,
|
||||||
|
"rotZ": 0.0,
|
||||||
|
"scaleX": 1.0,
|
||||||
|
"scaleY": 1.0,
|
||||||
|
"scaleZ": 1.0
|
||||||
|
},
|
||||||
|
"Nickname": "",
|
||||||
|
"Description": "",
|
||||||
|
"ColorDiffuse": {
|
||||||
|
"r": 0.713239133,
|
||||||
|
"g": 0.713239133,
|
||||||
|
"b": 0.713239133
|
||||||
|
},
|
||||||
|
"Grid": true,
|
||||||
|
"Locked": false,
|
||||||
|
"SidewaysCard": false,
|
||||||
|
"DeckIDs": [],
|
||||||
|
"CustomDeck": {
|
||||||
|
"1": {
|
||||||
|
"FaceURL": "SET ME",
|
||||||
|
"BackURL": "SET ME"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ContainedObjects": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user