#!/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 makeCardJson(nickname, description, cardID, outJson=None): card = CARD_JSON_TEMPLATE.copy() card.update({"Nickname": nickname, "Description": description, "CardID": 100 + cardID}) if outJson is not None: outJson['ObjectStates'][0]['DeckIDs'].append(100 + 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 addCardToBase(svg, baseImg, cardsPerRow, cardNum): for e in svg.findall('{http://www.w3.org/2000/svg}g'): e.set("transform", "{} translate({} {})".format( e.get("transform", ""), str((cardNum % cardsPerRow) * CARD_WIDTH), str(int(cardNum / cardsPerRow) * CARD_HEIGHT))) baseImg.getroot().append(svg.getroot()) def makeFace(outSVG, cardsPerRow, cardNum, deckType, cardFile, card): path = os.path.join(bundle_dir, "images", deckType, cardFile) fig = makeSVG(path, card) addCardToBase(fig, outSVG, cardsPerRow, cardNum) def makeDoubleSidedFace(outSVG, cardsPerRow, cardNum, deckType, cardFile, cardFront, cardBack): makeFace(outSVG, cardsPerRow, cardNum, deckType, cardFile, cardFront) makeFace(outSVG, cardsPerRow, cardNum + 1, deckType, cardFile, cardBack) def makeCards(deckJson, outfile, outdir, host): # number of cards in x and y direction deckType = deckJson["type"] if deckType in ["hero", "villain"]: cardsPerRow = math.ceil(math.sqrt(len(deckJson['deck']) + len(deckJson['character']) * 2)) else: cardsPerRow = math.ceil(math.sqrt(len(deckJson['deck']) * 2)) outSVG = etree.ElementTree( etree.Element('svg', attrib={'width': str(cardsPerRow * CARD_WIDTH) + "pt", 'height': str(cardsPerRow * CARD_HEIGHT) + "pt", 'version': "1.2", 'xmlns': "http://www.w3.org/2000/svg"})) with open(bundle_dir + "/templates/deck.json") as f: outJson = json.load(f) outJson['ObjectStates'][0]['CustomDeck']['1'].update( {"NumWidth": cardsPerRow, "NumHeight": cardsPerRow, "FaceURL": host + outfile + ".png", "BackURL": "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"}) cardNum = 0 # Make a card for each hero character card if deckType == "hero": for card in deckJson['character']: makeDoubleSidedCardJson(card['name'], "Active", "Incapacitated", cardNum, outJson) makeFace(outSVG, cardsPerRow, cardNum, deckType, "charFront.svg", card) cardNum += 1 makeFace(outSVG, cardsPerRow, 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"] # character card makeDoubleSidedCardJson(card['name'], "Front", "Back", cardNum, outJson) makeDoubleSidedFace(outSVG, cardsPerRow, cardNum, deckType, "character.svg", front, back) cardNum += 2 # instructions card makeDoubleSidedCardJson(card['name'] + " instructions", "Front", "Back", cardNum, outJson) makeDoubleSidedFace(outSVG, cardsPerRow, cardNum, deckType, "instructions.svg", front, back) cardNum += 2 # Make a card for each normal card for card in deckJson['deck']: for i in range(0, card.get('count', 1)): makeCardJson(card['name'], card.get('keywords', ""), cardNum, outJson) makeFace(outSVG, cardsPerRow, cardNum, deckType, "card.svg", card) cardNum += 1 # write SVG and convert to PNG outSVG.write(os.path.join(outdir, outfile) + ".svg") command = ["inkscape", "-z", "-f", os.path.join(outdir, outfile) + ".svg", "-w", str(cardsPerRow * CARD_WIDTH * 5), "-e", os.path.join(outdir, outfile) + ".png"] print("To regenerate PNG after editing SVG, run:\n " + " ".join(command)) subprocess.run(command) # Write TTS deck json with open(os.path.join(outdir, 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) makeCards(deckJson, outfile, os.getcwd(), "file://" + os.getcwd() + "/")