Rewrite for SVG support
heros and environments only at the moment
This commit is contained in:
parent
810369596d
commit
78ac4d1da3
|
@ -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
|
||||||
for card in deckJson['character']:
|
if cardType == "hero":
|
||||||
cardFrontImg = IMG_HERO_CHAR_FRONT.copy()
|
for card in deckJson['character']:
|
||||||
draw = ImageDraw.Draw(cardFrontImg)
|
makeFace(baseImage, baseX, cardNum,
|
||||||
drawTextIf(draw, (150, 70), FONT_CHAR_HP, card, 'name')
|
os.path.join("images", cardType, "charFront.svg"), card)
|
||||||
drawTextIf(draw, (100, 900), FONT_DESCRIPTION, card, 'power', 40)
|
cardNum += 1
|
||||||
drawTextIf(draw, (53, 260), FONT_CHAR_HP, card, 'hp')
|
|
||||||
addCardToBase(baseImage, baseX, cardNum, cardFrontImg)
|
|
||||||
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')
|
cardNum += 1
|
||||||
drawTextIf(draw, (80, 800), FONT_DESCRIPTION, card, 'incapacitated', 43)
|
|
||||||
addCardToBase(baseImage, baseX, cardNum, cardBackImg)
|
|
||||||
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/"})
|
||||||
|
@ -111,7 +116,7 @@ def makeJson(deckJson, imgWidth, imgHeight, outfile):
|
||||||
# add front card with back as second state
|
# add front card with back as second state
|
||||||
cardOutBack = cardTemplate.copy()
|
cardOutBack = cardTemplate.copy()
|
||||||
cardOutBack.update({"Nickname": card['name'],
|
cardOutBack.update({"Nickname": card['name'],
|
||||||
"Description": "Incapacitated",
|
"Description": "Incapacitated",
|
||||||
"CardID": cardNum + 1})
|
"CardID": cardNum + 1})
|
||||||
cardOut = cardTemplate.copy()
|
cardOut = cardTemplate.copy()
|
||||||
cardOut.update({"Nickname": card['name'],
|
cardOut.update({"Nickname": card['name'],
|
||||||
|
@ -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)
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 409 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 492 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.5 MiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.5 MiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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
|
||||||
|
}
|
|
@ -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