diff --git a/.gitignore b/.gitignore index f48aa5b..49dc3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,7 @@ $RECYCLE.BIN/ BaBOC/ TSSSF/ WSotT/ + +# Key-containing config +# ========================= +imgur_auth.py diff --git a/PIL_Helper.py b/PIL_Helper.py index f9c22e0..7cf66ea 100644 --- a/PIL_Helper.py +++ b/PIL_Helper.py @@ -1,7 +1,14 @@ from PIL import Image, ImageFont, ImageDraw, ImageOps -import os, glob +from StringIO import StringIO +import os, glob, requests from math import ceil +class BadNetStatusException(Exception): + ''' + An exception for LoadImageFromURL to throw if it has + problems fetching the remote URL. + ''' + def BuildFont(fontname, fontsize): return ImageFont.truetype(fontname, fontsize) @@ -101,7 +108,7 @@ def AddText(image, text, font, fill=(0,0,0), anchor=(0,0), # If current line is blank, just change y and skip to next if not line == "": if padline == True: - line = " {0} ".format(line) + line = u" {0} ".format(line) line_width, line_height = font.getsize(line) if halign == "left": x_pos = start_x @@ -201,6 +208,12 @@ def BuildPage(card_list, grid_width, grid_height, filename, def BlankImage(w, h, color=(255,255,255), image_type="RGBA"): return Image.new(image_type, (w, h), color=color) +def LoadImageFromURL(url): + r = requests.get(url) + if r.status_code is not 200: + raise BadNetStatusException(r.status_code) + return Image.open(StringIO(r.content)) + def LoadImage(filepath, fallback="blank.png"): try: return Image.open(filepath) diff --git a/TSSSF_CardGen.py b/TSSSF_CardGen.py index 4a1e439..cbfce12 100644 --- a/TSSSF_CardGen.py +++ b/TSSSF_CardGen.py @@ -1,7 +1,7 @@ import os, glob, shutil, traceback, random import PIL_Helper -TYPE, PICTURE, SYMBOLS, TITLE, KEYWORDS, BODY, FLAVOR, EXPANSION, CLIENT = range(9) +TYPE, PICTURE, SYMBOLS, TITLE, KEYWORDS, BODY, FLAVOR, EXPANSION, COPYRIGHT = range(9) DIRECTORY = "TSSSF" ARTIST = "Pixel Prism" @@ -26,6 +26,7 @@ ExpansionIconsPath = ResourcePath + "/expansion icons/" CardBacksPath = ResourcePath + "/card backs/" FontsPath = ResourcePath + "/fonts/" +PlaceholderPath = ResourcePath + "/placeholder art/" VassalTemplatesPath = DIRECTORY + "/vassal templates/" VassalWorkspacePath = DIRECTORY + "/vassal workspace/" @@ -34,6 +35,7 @@ VassalCard = [0] ART_WIDTH = 600 +ART_HEIGHT = 443 base_w = 889 base_h = 1215 base_w_center = base_w / 2 @@ -44,6 +46,7 @@ textmaxwidth = 689 croprect = (50, 63, 788 + 50, 1088 + 63) +ART_CROPRECT=(0,0,ART_WIDTH, ART_HEIGHT) TextHeightThresholds = [363, 378, 600] TitleWidthThresholds = [50] # This is in #characters, fix later plox @@ -83,26 +86,26 @@ } ArtMissing = [ - PIL_Helper.LoadImage(CardPath + "artmissing01.png"), - PIL_Helper.LoadImage(CardPath + "artmissing02.png"), - PIL_Helper.LoadImage(CardPath + "artmissing03.png"), - PIL_Helper.LoadImage(CardPath + "artmissing04.png"), - PIL_Helper.LoadImage(CardPath + "artmissing05.png"), - PIL_Helper.LoadImage(CardPath + "artmissing06.png"), - PIL_Helper.LoadImage(CardPath + "artmissing07.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing01.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing02.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing03.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing04.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing05.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing06.png"), + PIL_Helper.LoadImage(PlaceholderPath + "artmissing07.png"), ] Frames = { - "START": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Start-bleed.png"), - "Warning": PIL_Helper.LoadImage(CardPath + "BLEED_Card - Warning.png"), - "Pony": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Pony-bleed.png"), - "Ship": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Ship-bleed.png"), - "Rules1": PIL_Helper.LoadImage(CardPath + "BLEED_Rules1.png"), - "Rules3": PIL_Helper.LoadImage(CardPath + "BLEED_Rules3.png"), - "Rules5": PIL_Helper.LoadImage(CardPath + "BLEED_Rules5.png"), - "Goal": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Goal-bleed.png"), - "Derpy": PIL_Helper.LoadImage(CardPath + "BLEED_Card - Derpy Hooves.png"), - "TestSubject": PIL_Helper.LoadImage(CardPath + "BLEED_Card - OverlayTest Subject Cheerilee.png") + "start": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Start-bleed.png"), + "warning": PIL_Helper.LoadImage(CardPath + "BLEED_Card - Warning.png"), + "pony": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Pony-bleed.png"), + "ship": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Ship-bleed.png"), + "rules1": PIL_Helper.LoadImage(CardPath + "BLEED_Rules1.png"), + "rules3": PIL_Helper.LoadImage(CardPath + "BLEED_Rules3.png"), + "rules5": PIL_Helper.LoadImage(CardPath + "BLEED_Rules5.png"), + "goal": PIL_Helper.LoadImage(BleedTemplatesPath + "BLEED-Blank-Goal-bleed.png"), + "derpy": PIL_Helper.LoadImage(CardPath + "BLEED_Card - Derpy Hooves.png"), + "testsubject": PIL_Helper.LoadImage(CardPath + "BLEED_Card - OverlayTest Subject Cheerilee.png") } Symbols = { @@ -110,6 +113,7 @@ "female": PIL_Helper.LoadImage(SymbolsPath + "Symbol-Female.png"), "malefemale": PIL_Helper.LoadImage(SymbolsPath + "Symbol-MaleFemale.png"), "earth pony": PIL_Helper.LoadImage(SymbolsPath + "Symbol-Earth-Pony.png"), + "earthpony": PIL_Helper.LoadImage(SymbolsPath + "Symbol-Earth-Pony.png"), "unicorn": PIL_Helper.LoadImage(SymbolsPath + "Symbol-Unicorn.png"), "uniearth": PIL_Helper.LoadImage(SymbolsPath + "symbol-uniearth.png"), "pegasus": PIL_Helper.LoadImage(SymbolsPath + "Symbol-Pegasus.png"), @@ -129,7 +133,7 @@ "3-4": PIL_Helper.LoadImage(SymbolsPath + "symbol-34.png"), "2-3": PIL_Helper.LoadImage(SymbolsPath + "symbol-23.png") } -TIMELINE_SYMBOL_LIST = ["Dystopian"] +TIMELINE_SYMBOL_LIST = ["dystopian"] Expansions = { "Everfree14": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-Everfree14.png"), @@ -155,7 +159,10 @@ "Ponycon 2015": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-ponynyc.png"), "Patreon": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-Patreon.png"), "Gameshow": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-gameshow.png"), - "BABScon": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-BABScon.png") + "BABScon": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-BABScon.png"), + "web-outline": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-web-circledark.png"), + "web-white": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-www.png"), + "web-grey": PIL_Helper.LoadImage(ExpansionIconsPath + "symbol-web-circlegrey.png") } ColorDict = { @@ -196,22 +203,22 @@ } backs = { - "START": PIL_Helper.LoadImage(CardBacksPath + "Back-Start.png"), - "Pony": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), - "Goal": PIL_Helper.LoadImage(CardBacksPath + "Back-Goals.png"), - "Ship": PIL_Helper.LoadImage(CardBacksPath + "Back-Ships.png"), - "Card": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), - "Shipwrecker": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), - "BLANK": PIL_Helper.LoadImage(CardBacksPath + "Blank - Intentionally Left Blank.png"), - "Rules1": PIL_Helper.LoadImage(CardPath + "Rules2.png"), - "Rules3": PIL_Helper.LoadImage(CardPath + "Rules4.png"), - "Rules5": PIL_Helper.LoadImage(CardPath + "Rules6.png"), - "TestSubject": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), - "Warning": PIL_Helper.LoadImage(CardPath + "Card - Contact.png") + "start": PIL_Helper.LoadImage(CardBacksPath + "Back-Start.png"), + "pony": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), + "goal": PIL_Helper.LoadImage(CardBacksPath + "Back-Goals.png"), + "ship": PIL_Helper.LoadImage(CardBacksPath + "Back-Ships.png"), + "card": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), + "shipwrecker": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), + "blank": PIL_Helper.LoadImage(CardBacksPath + "Blank - Intentionally Left Blank.png"), + "rules1": PIL_Helper.LoadImage(CardPath + "Rules2.png"), + "rules3": PIL_Helper.LoadImage(CardPath + "Rules4.png"), + "rules5": PIL_Helper.LoadImage(CardPath + "Rules6.png"), + "testsubject": PIL_Helper.LoadImage(CardBacksPath + "Back-Main.png"), + "warning": PIL_Helper.LoadImage(CardPath + "Card - Contact.png") } -special_card_types = ["Rules1", "Rules3", "Rules5", "Warning", "Derpy", "Card"] -special_cards_with_copyright = ["Derpy"] +special_card_types = ["rules1", "rules3", "rules5", "warning", "derpy", "card"] +special_cards_with_copyright = ["derpy"] def FixFileName(tagin): @@ -262,6 +269,13 @@ def SaveCard(filepath, image_to_save): filepath = "{}_{:>03}{}".format(basepath, i, extension) image_to_save.save(filepath, dpi=(300, 300)) +def BuildSingleCard(linein): + tags = linein.strip('\n').strip('\r').replace(r'\n', '\n').split('`') + im_bleed = PickCardFunc(tags[TYPE], tags) + im_crop = im_bleed.crop(croprect) + im_vassal = PIL_Helper.ResizeImage(im_crop, VASSAL_SCALE) + + return (im_bleed, im_crop, im_vassal) def BuildCard(data): picture = None @@ -304,26 +318,27 @@ def BuildCard(data): def BuildBack(data): if type(data).__name__ == 'dict': - card_type = data['type'] + card_type = data['type'].lower() else: card = data.strip('\n').strip('\r').replace(r'\n', '\n').split('`') - card_type = card[TYPE] + card_type = card[TYPE].lower() return backs[card_type] def PickCardFunc(card_type, data): - if card_type == "START": + card_type = card_type.lower() + if card_type == "start": return MakeStartCard(data) - elif card_type == "Pony": + elif card_type == "pony": return MakePonyCard(data) - elif card_type == "Ship": + elif card_type == "ship": return MakeShipCard(data) - elif card_type == "Goal": + elif card_type == "goal": return MakeGoalCard(data) - elif card_type == "BLANK": + elif card_type == "blank": return MakeBlankCard() - elif card_type == "TestSubject": + elif card_type == "testsubject": return MakePonyCard(data) elif card_type in special_card_types: return MakeSpecialCard(data) @@ -332,25 +347,38 @@ def PickCardFunc(card_type, data): def GetFrame(card_type): - return Frames[card_type].copy() + return Frames[card_type.lower()].copy() def AddCardArt(image, filename, anchor): if filename == "NOART": return - if os.path.exists(os.path.join(CardPath, filename)): + if filename.startswith("http"): + try: + art = PIL_Helper.LoadImageFromURL(filename) + except PIL_Helper.BadNetStatusException as e: + art = random.choice(ArtMissing) + elif os.path.exists(os.path.join(CardPath, filename)) and filename != "": art = PIL_Helper.LoadImage(os.path.join(CardPath, filename)) else: art = random.choice(ArtMissing) # Find desired height of image based on width of 600 px w, h = art.size - h = int((float(ART_WIDTH) / w) * h) - # Resize image to fit in frame - art = PIL_Helper.ResizeImage(art, (ART_WIDTH, h)) + if float(w) / float(h) < float(ART_WIDTH) / float(ART_HEIGHT): + h = int((float(ART_WIDTH) / w) * h) + # Resize image to fit in frame + art = PIL_Helper.ResizeImage(art, (ART_WIDTH, h)) + else: + w = int((float(ART_HEIGHT) / h) * w) + # Resize image to fit in frame + art = PIL_Helper.ResizeImage(art, (w, ART_HEIGHT)) + + art = art.crop(ART_CROPRECT) image.paste(art, anchor) def AddSymbols(image, symbols, card_type=""): + symbols = [x.lower() for x in symbols] # Remove any timeline symbols from the symbols list pruned_symbols = set(symbols) - set(TIMELINE_SYMBOL_LIST) if card_type == "Goal": @@ -365,7 +393,7 @@ def AddSymbols(image, symbols, card_type=""): positions = [Anchors["Symbol1"], Anchors["Symbol2"]] for index, s in enumerate(symbols): - sym = Symbols.get(s.lower(), None) + sym = Symbols.get(s, None) if sym: if s in TIMELINE_SYMBOL_LIST: image.paste(sym, Anchors["TimelineSymbol"], sym) @@ -491,15 +519,18 @@ def CopyrightText(card, image, color, artist): if type(card).__name__ == 'dict': client = card.get('client') else: - if len(card) - 1 >= CLIENT: - client = str(card[CLIENT]) + if len(card) - 1 >= COPYRIGHT: + client = unicode(card[COPYRIGHT]) if client is not None: card_set += " " + client - text = "{}; TSSSF by Horrible People Games. Art by {}.".format( - card_set, - artist - ) + if "TSSSF by Horrible People Games" in client: + text = client + else: + text = "{}; TSSSF by Horrible People Games. Art by {}.".format( + card_set, + artist + ) PIL_Helper.AddText( image=image, text=text, @@ -673,7 +704,7 @@ def MakeSpecialCard(card): def MakeSpecialCardJSON(data): print repr(data['picture']) image = GetFrame(data['picture']) - if data['picture'] in special_cards_with_copyright: + if data['picture'].lower() in special_cards_with_copyright: CopyrightText(data, image, ColorDict["Copyright"], data.get('artist', ARTIST)) if Expansion_Icon is not None: AddExpansionJSON(image, Expansion_Icon) @@ -683,7 +714,7 @@ def MakeSpecialCardJSON(data): def MakeSpecialCardPON(data): print repr(data[PICTURE]) image = GetFrame(data[PICTURE]) - if data[PICTURE] in special_cards_with_copyright: + if data[PICTURE].lower() in special_cards_with_copyright: CopyrightText(data, image, ColorDict["Copyright"], ARTIST) if len(data) > EXPANSION: AddExpansion(image, data[EXPANSION]) diff --git a/imgur_auth.py b/imgur_auth.py new file mode 100644 index 0000000..14c7462 --- /dev/null +++ b/imgur_auth.py @@ -0,0 +1,4 @@ +#If you wish to use imgur-save functionality, put your API key here +CLIENT_ID = '' +CLIENT_SECRET = '' + diff --git a/single_card.py b/single_card.py new file mode 100755 index 0000000..fafbe7e --- /dev/null +++ b/single_card.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +''' +Generate a single card +''' +import argparse +import base64 +import traceback +import sys +import urllib +import requests +import TSSSF_CardGen +import json +import imgur_auth +import re +from StringIO import StringIO + + +def SaveCardToFile(image_object, location): + image_object.save(location, format="PNG", dpi=(300, 300)) + return location + + +def SaveCardToURL(image_object): + fileobj = StringIO() + image_object.save(fileobj, format="PNG", dpi=(300, 300)) + encoded_image = fileobj.getvalue().encode("base64") + return("data:image/png;base64," + urllib.quote(encoded_image)) + + +def GetImgurCredits(): + credits = requests.get( + 'https://api.imgur.com/3/credits.json', + headers={'Authorization': 'Client-ID %s' % imgur_auth.CLIENT_ID}, + data={'key': imgur_auth.CLIENT_SECRET} + ) + print "Full GetCredits retval: %r" % credits.text + return json.loads(credits.text)["data"]["ClientRemaining"] + + +def SaveCardToImgur(image_object, title=None, desc=None): + #Make sure we have the budget to do this + if GetImgurCredits() < 10: + raise ValueError("Insufficient imgur credits remaining") + fileobj = StringIO() + image_object.save(fileobj, format="PNG", dpi=(300, 300)) + + img_json = requests.post( + 'https://api.imgur.com/3/upload.json', + headers={'Authorization': 'Client-ID %s' % imgur_auth.CLIENT_ID}, + data={ + 'key': imgur_auth.CLIENT_SECRET, + 'title': title or 'Card generated with TSSSF Card Generator', + 'description': desc or '', + 'type': 'base64', + 'image': fileobj.getvalue().encode("base64") + } + ) + #return img_json.text + return json.loads(img_json.text)["data"]["id"] + + +def SaveCard(image, save_type, location=None, imgurtitle=None, imgurdesc=None): + if save_type == "file": + retval = SaveCardToFile(image, location) + elif save_type == "encoded_url": + retval = SaveCardToURL(image) + elif save_type == "imgur": + retval = SaveCardToImgur(image, imgurtitle, imgurdesc) + else: + raise ValueError("save type not recognized") + return retval + + +def make_single_card(card_line, output_file, image_type, save_type, + imgurtitle, imgurdesc): + im = {} + + print("Attempting to build card %r" % card_line) + (im["bleed"], + im["cropped"], + im["vassal"]) = TSSSF_CardGen.BuildSingleCard(card_line) + + return SaveCard(im[image_type], save_type, output_file, imgurtitle, + imgurdesc) + +if __name__ == '__main__': + ACTUAL_STDOUT = sys.stdout + sys.stdout = sys.stderr + parser = argparse.ArgumentParser(prog="single_card.py") + + parser.add_argument('-c', '--card_line', + help="Base64-encoded single-line PON card definition", + required=True) + parser.add_argument('-o', '--output', + help="File to write card to", + default=None) + parser.add_argument('-i', '--imagetype', + help="Set image type to output", + choices=("bleed", "cropped", "vassal"), + default="cropped") + parser.add_argument('-r', '--returntype', + help="Output format", + choices=("file", "encoded_url", "imgur"), + default="cropped") + parser.add_argument('-t', '--imgurtitle', + help="Base64-encoded alternate imgur title", + default=None) + parser.add_argument('-d', '--imgurdesc', + help="Base64-encoded alternate imgur description", + default=None) + + args = parser.parse_args() + + if args.returntype == "file" and args.output is None: + parser.error("--output must be defined if --returntype is set to file") + + try: + CARD_LINE = base64.b64decode(args.card_line).decode('utf-8') + IMGURTITLE = args.imgurtitle + if IMGURTITLE is not None: + IMGURTITLE = base64.b64decode(IMGURTITLE) + IMGURDESC = args.imgurdesc + if IMGURDESC is not None: + IMGURDESC = base64.b64decode(IMGURDESC) + except Exception: + print(traceback.format_exc()) + print("Failed to base64 decode a string") + sys.exit(1) + OUTPUT = "" + try: + OUTPUT = make_single_card(CARD_LINE, args.output, args.imagetype, + args.returntype, IMGURTITLE, IMGURDESC) + except Exception: + print(traceback.format_exc()) + print("Failed to build single card %r" % CARD_LINE) + sys.exit(1) + print >> ACTUAL_STDOUT, OUTPUT + print("Success!") + sys.exit(0)