#!/usr/bin/env python3 # Note: SVG templates are heavily based on these: # https://boardgamegeek.com/thread/813176/card-templates/page/1 import json import math import os import subprocess import sys from lxml import etree if getattr(sys, 'frozen', False): # we are running in a bundle bundle_dir = sys._MEIPASS else: # we are running in a normal Python environment bundle_dir = os.path.dirname(os.path.abspath(__file__)) CARD_WIDTH = 181 CARD_HEIGHT = 253 with open(bundle_dir + "/templates/card.json") as f: CARD_JSON_TEMPLATE = json.load(f) def setText(tree, id, text): element = tree.find('.//*[@id="' + id + '"]') if element is None: print("id", id, "not found") return elif element.tag == "{http://www.w3.org/2000/svg}flowRoot": for e in element.findall("{http://www.w3.org/2000/svg}flowPara"): element.remove(e) # clear child paragraphs lines = str(text).splitlines() for line in lines: etree.SubElement(element, "{http://www.w3.org/2000/svg}flowPara").text=line else: element.text = str(text) def removeElement(tree, id): mark = tree.find('.//*[@id="' + id + '"]') if mark is not None: mark.getparent().remove(mark) def makeSVG(base, properties): tree = etree.parse(base) for id, text in properties.items(): if id not in ["count", "art"]: setText(tree, id, text) # remove HP mark if there is no hp if "hp" not in properties: removeElement(tree, "hpMark") # remove setup box if there is no setup if "setup" not in properties: removeElement(tree, "setupBox") 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): for e in svg.findall('{http://www.w3.org/2000/svg}g'): e.set("transform", "{} translate({} {})".format( e.get("transform", ""), str((cardNum % baseX) * CARD_WIDTH), str(int(cardNum / baseX) * CARD_HEIGHT))) baseImg.getroot().append(svg.getroot()) def makeFace(baseImage, baseX, cardNum, deckType, cardFile, card): path = os.path.join(bundle_dir, "images", deckType, cardFile) fig = makeSVG(path, card) addCardToBase(fig, baseImage, baseX, cardNum) def makeFaces(deckJson, outfile): baseX = math.ceil(math.sqrt(len(deckJson['deck']) + len(deckJson['character']) * 2)) baseImage = etree.ElementTree( etree.Element('svg', attrib={'width': str(baseX * CARD_WIDTH) + "pt", 'height': str(baseX * CARD_HEIGHT) + "pt", 'version': "1.2", 'xmlns': "http://www.w3.org/2000/svg"})) deckType = deckJson["type"] cardNum = 0 # Make a card for each hero character card if deckType == "hero": for card in deckJson['character']: makeFace(baseImage, baseX, cardNum, deckType, "charFront.svg", card) cardNum += 1 makeFace(baseImage, baseX, cardNum, deckType, "charBack.svg", card) cardNum += 1 # Make a character and instructions card for each villain card elif deckType == "villain": for card in deckJson['character']: front = card["front"] front["name"] = card["name"] back = card["back"] back["name"] = card["name"] makeFace(baseImage, baseX, cardNum, deckType, "character.svg", front) cardNum += 1 makeFace(baseImage, baseX, cardNum, deckType, "character.svg", back) cardNum += 1 makeFace(baseImage, baseX, cardNum, deckType, "instructions.svg", front) cardNum += 1 makeFace(baseImage, baseX, cardNum, deckType, "instructions.svg", back) cardNum += 1 # Make a card for each card for card in deckJson['deck']: makeFace(baseImage, baseX, cardNum, deckType, "card.svg", card) cardNum += 1 baseImage.write(outfile + ".svg") command = ["inkscape", "-z", "-f", outfile + ".svg", "-w", str(baseX * CARD_WIDTH * 5), "-e", outfile + ".png"] print("To regenerate PNG after editing SVG, run:\n " + " ".join(command)) subprocess.run(command) return baseX def makeCardJson(nickname, description, cardID, outJson=None): card = CARD_JSON_TEMPLATE.copy() card.update({"Nickname": nickname, "Description": description, "CardID": cardID}) if outJson is not None: outJson['ObjectStates'][0]['DeckIDs'].append(cardID) outJson['ObjectStates'][0]['ContainedObjects'].append(card) return card def makeDoubleSidedCardJson(nickname, descriptionFront, descriptionBack, cardID, outJson): cardBack = makeCardJson(nickname, descriptionBack, cardID + 1) card = makeCardJson(nickname, descriptionFront, cardID, outJson) card.update({ "States": {"2": cardBack}}) return card def makeJson(deckJson, imgWidth, outfile): with open(bundle_dir + "/templates/deck.json") as f: outJson = json.load(f) # number of cards in x and y direction outJson['ObjectStates'][0]['CustomDeck']['1'].update( {"NumWidth": imgWidth, "NumHeight": imgWidth, #"FaceURL": "http://adam-desktop.dyn.wpi.edu:8000/" + outfile + ".png", "FaceURL": "file://" + os.getcwd() + "/" + outfile + ".png", "BackURL": "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"}) # decks start at (10 * deck id) cardNum = 100 if deckJson["type"] == "hero": for card in deckJson['character']: # character card makeDoubleSidedCardJson(card['name'], "Active", "Incapacitated", cardNum, outJson) cardNum += 2 elif deckJson["type"] == "villain": for card in deckJson['character']: # character card makeDoubleSidedCardJson(card['name'], "Front", "Back", cardNum, outJson) cardNum += 2 # instructions card makeDoubleSidedCardJson(card['name'] + " instructions", "Front", "Back", cardNum, outJson) cardNum += 2 for card in deckJson['deck']: for i in range(0, card.get('count', 1)): # normal cards makeCardJson(card['name'], card.get('keywords', ""), cardNum, outJson) cardNum += 1 with open(outfile + ".json", "w") as f: json.dump(outJson, f) if __name__ == '__main__': if len(sys.argv) < 3: print("not enough arguments!") inputJson = input("Input file: ") outfile = input("Output file (no suffix): ") else: inputJson = sys.argv[1] outfile = sys.argv[2] with open(inputJson) as f: deckJson = json.load(f) imgWidth = makeFaces(deckJson, outfile) makeJson(deckJson, imgWidth, outfile)