diff --git a/README.md b/README.md index a2945ba..e629c68 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Trademark and copyright Wizards of the Coast 2022. Templates for this project in * I have included a copy of the font in this repo which tweaks the asterisk symbol to match how it appears in the power / toughness of real cards, * You can download the original from Wizards' website [here](https://magic.wizards.com/sites/all/themes/wiz_mtg/fonts/Beleren/Beleren2016-Bold.ttf), * My custom Magic symbols font `NDPMTG.ttf`, included in the repo, - * [Keyrune](https://keyrune.andrewgioia.com/) and [Mana](https://mana.andrewgioia.com/), for the expansion symbol and transform symbols, + * [Mana](https://mana.andrewgioia.com/), for transform symbols, * Relay Medium and Calibri. * A standard installation of [Python 3](https://www.python.org/downloads/). @@ -31,7 +31,7 @@ Trademark and copyright Wizards of the Coast 2022. Templates for this project in * **Optional**: Copy the files from `/scripts/utils` to the `Scripts` folder in your Photoshop installation. For me, this was `C:\Program Files\Adobe\Adobe Photoshop CC 2018\Presets\Scripts`. Modify the paths in those files to point to the corresponding files in `/scripts`. This enables the use of a few utility scripts which are handy when making renders manually. # FAQ -* *I want to change the set symbol to something else.* Head over to https://andrewgioia.github.io/Keyrune/cheatsheet.html - you can use any of these symbols for the set symbol for your cards. Copy the text of the symbol you want on the cheatsheet, then replace the expansion symbol character in quotations at the top of the file with the character you copied. +* *I want to customise the card's set symbol.* By default, the system draws the set symbol from `/scripts/icons/default.svg`, so if you'd like to use a different set symbol for all cards, you should replace this file with your desired one. The system also supports retrieving & using the set symbol for the card you're rendering - in `settings.jsx`, set `use_default_expansion_symbol` to `false`. The expansion symbol may not look 100% correct (as printed on real cards) due to inconsistencies in how set symbols are sized, positioned, and outlined on real cards. * *I'm getting an error message saying that the Python call failed and `card.json` was not created.* This is a result of the Python command not executing properly on your computer. The error message contains a copy of the command the system attempted - copy this command and try running it from the command line to diagnose the issue. You may need to adjust the Python command defined in `settings.jsx` depending on how your computer's Python installation is configured. The default commands are: * Windows: `python ...` * macOS: `/usr/local/bin/python3 ...` diff --git a/scripts/constants.jsx b/scripts/constants.jsx index 25dff1a..9cbf8b2 100644 --- a/scripts/constants.jsx +++ b/scripts/constants.jsx @@ -2,6 +2,9 @@ var json_file_path = "/scripts/card.json"; var image_file_path = "/scripts/card.jpg"; +var icon_directory = "/scripts/icons/"; +var default_icon_name = "default"; +var dot_svg = ".svg"; // Card classes - finer grained than Scryfall layouts var normal_class = "normal"; @@ -21,7 +24,6 @@ var basic_class = "basic"; var planar_class = "planar"; var token_class = "token"; - // Layer names var LayerNames = { WHITE: "W", @@ -91,7 +93,7 @@ var LayerNames = { TYPE_LINE_ADVENTURE: "Typeline - Adventure", MANA_COST: "Mana Cost", MANA_COST_ADVENTURE: "Mana Cost - Adventure", - EXPANSION_SYMBOL: "Expansion Symbol", + EXPANSION_SYMBOL: "Expansion Symbol", // group name COLOUR_INDICATOR: "Colour Indicator", POWER_TOUGHNESS: "Power / Toughness", FLIPSIDE_POWER_TOUGHNESS: "Flipside Power / Toughness", @@ -117,6 +119,7 @@ var LayerNames = { MUTATE_REFERENCE: "Mutate Reference", PT_REFERENCE: "PT Adjustment Reference", PT_TOP_REFERENCE: "PT Top Reference", + EXPANSION_REFERENCE: "Expansion Reference", // planeswalker FIRST_ABILITY: "First Ability", diff --git a/scripts/get_card_info.py b/scripts/get_card_info.py index 1c2b5c4..31fe695 100644 --- a/scripts/get_card_info.py +++ b/scripts/get_card_info.py @@ -1,7 +1,8 @@ -import time -import sys import json -from urllib import request, parse, error +import os +import sys +import time +from urllib import error, parse, request def add_meld_info(card_json): @@ -30,7 +31,7 @@ def add_meld_info(card_json): # If the card specifies which set to retrieve the scan from, do that try: pipe_idx = card_name.index("$") - card_set = card_name[pipe_idx + 1:] + card_set = card_name[pipe_idx + 1 :] card_name = card_name[0:pipe_idx] print(f"Searching Scryfall for: {card_name}, set: {card_set}...", end="", flush=True) card = request.urlopen( @@ -39,9 +40,7 @@ def add_meld_info(card_json): except ValueError: print(f"Searching Scryfall for: {card_name}...", end="", flush=True) - card = request.urlopen( - f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(card_name)}" - ).read() + card = request.urlopen(f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(card_name)}").read() except error.HTTPError: input("\nError occurred while attempting to query Scryfall. Press enter to exit.") @@ -49,7 +48,23 @@ def add_meld_info(card_json): card_json = add_meld_info(json.loads(card)) json_dump = json.dumps(card_json) - with open(sys.path[0] + "/card.json", 'w') as f: + with open(sys.path[0] + "/card.json", "w") as f: json.dump(json_dump, f) - print(" and done!", flush=True) + + set_code = card_json["set"].upper() + icons_folder = sys.path[0] + "/icons" + if not os.path.exists(icons_folder): + os.mkdir(icons_folder) + icon_path = icons_folder + f"/{set_code}.svg" + if not os.path.exists(icon_path): + try: + print(f"Searching Scryfall for the icon for the set: {set_code}...", end="", flush=True) + set_info = json.loads(request.urlopen(f"https://api.scryfall.com/sets/{set_code}").read()) + request.urlretrieve(set_info["icon_svg_uri"], icon_path) + + except error.HTTPError: + input("\nError occurred while attempting to query Scryfall. Press enter to exit.") + print(" and done!", flush=True) + else: + print(f"Icon for the set {set_code} already exists - not going to retrieve from Scryfall again.", flush=True) diff --git a/scripts/helpers.jsx b/scripts/helpers.jsx index 99be0e6..eb47727 100644 --- a/scripts/helpers.jsx +++ b/scripts/helpers.jsx @@ -101,7 +101,7 @@ function align(align_type) { desc.putReference(idnull, ref); var idUsng = charIDToTypeID("Usng"); var idADSt = charIDToTypeID("ADSt"); - var idAdCH = charIDToTypeID(align_type); // align type - "AdCV" for vertical, "AdCH" for horizontal + var idAdCH = charIDToTypeID(align_type); desc.putEnumerated(idUsng, idADSt, idAdCH); executeAction(idAlgn, desc, DialogModes.NO); } @@ -122,6 +122,38 @@ function align_horizontal() { align("AdCH"); } +function align_left() { + /** + * Align the currently active layer to the left of the current selection. + */ + + align("AdLf"); +} + +function align_right() { + /** + * Align the currently active layer to the right of the current selection. + */ + + align("AdRg"); +} + +function align_top() { + /** + * Align the currently active layer to the top of the current selection. + */ + + align("AdTp"); +} + +function align_bottom() { + /** + * Align the currently active layer to the bottom of the current selection. + */ + + align("AdBt"); +} + function frame_layer(layer, reference_layer) { /** * Scale a layer equally to the bounds of a reference layer, then centre the layer vertically and horizontally @@ -220,9 +252,9 @@ function disable_active_vector_mask() { set_active_vector_mask(false); } -function apply_stroke(stroke_weight, stroke_colour) { +function apply_stroke(stroke_weight, stroke_colour, stroke_type) { /** - * Applies an outer stroke to the active layer with the specified weight and colour. + * Applies a stroke to the active layer with the specified weight and colour at the specified position. */ idsetd = charIDToTypeID("setd"); @@ -248,7 +280,7 @@ function apply_stroke(stroke_weight, stroke_colour) { desc610.putBoolean(idenab, true); var idStyl = charIDToTypeID("Styl"); var idFStl = charIDToTypeID("FStl"); - var idInsF = charIDToTypeID("OutF"); + var idInsF = charIDToTypeID(stroke_type); desc610.putEnumerated(idStyl, idFStl, idInsF); idPntT = charIDToTypeID("PntT"); var idFrFl = charIDToTypeID("FrFl"); @@ -281,6 +313,30 @@ function apply_stroke(stroke_weight, stroke_colour) { executeAction(idsetd, desc608, DialogModes.NO); } +function apply_outer_stroke(stroke_weight, stroke_colour) { + /** + * Applies an outer stroke to the active layer with the specified weight and colour. + */ + + apply_stroke(stroke_weight, stroke_colour, "OutF"); +} + +function apply_inner_stroke(stroke_weight, stroke_colour) { + /** + * Applies an inner stroke to the active layer with the specified weight and colour. + */ + + apply_stroke(stroke_weight, stroke_colour, "InsF"); +} + +function apply_centre_stroke(stroke_weight, stroke_colour) { + /** + * Applies a centre stroke to the active layer with the specified weight and colour. + */ + + apply_stroke(stroke_weight, stroke_colour, "CtrF"); +} + function save_and_close(file_name, file_path) { /** * Saves the current document to the output folder (/out/) as a PNG and closes the document without saving. @@ -478,4 +534,4 @@ function insert_scryfall_scan(image_url, file_path) { var scryfall_scan = retrieve_scryfall_scan(image_url, file_path); return paste_file_into_new_layer(scryfall_scan); -} \ No newline at end of file +} diff --git a/scripts/icons/default.svg b/scripts/icons/default.svg new file mode 100644 index 0000000..063d356 --- /dev/null +++ b/scripts/icons/default.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/layouts.jsx b/scripts/layouts.jsx index 9b72c6f..0e8c6ec 100644 --- a/scripts/layouts.jsx +++ b/scripts/layouts.jsx @@ -43,6 +43,7 @@ var BaseLayout = Class({ * At minimum, the extending class should set this.name, this.oracle_text, this.type_line, and this.mana_cost. */ + this.set_code = this.scryfall.set; this.rarity = this.scryfall.rarity; this.artist = this.scryfall.artist; this.colour_identity = this.scryfall.color_identity; diff --git a/scripts/templates.jsx b/scripts/templates.jsx index b985278..1129594 100644 --- a/scripts/templates.jsx +++ b/scripts/templates.jsx @@ -46,6 +46,7 @@ var BaseTemplate = Class({ this.layout = layout; this.file = file; + this.file_path = file_path; this.load_template(file_path); @@ -75,12 +76,12 @@ var BaseTemplate = Class({ return ""; }, - load_template: function (file_path) { + load_template: function () { /** * Opens the template's PSD file in Photoshop. */ - var template_path = file_path + "/templates/" + this.template_file_name() + ".psd" + var template_path = this.file_path + "/templates/" + this.template_file_name() + ".psd" var template_file = new File(template_path); try { app.open(template_file); @@ -204,9 +205,11 @@ var ChilliBaseTemplate = Class({ reference_layer = mana_cost, ), new ExpansionSymbolField( - layer = expansion_symbol, - text_contents = expansion_symbol_character, + layer_group = expansion_symbol, + set_code = this.layout.set_code, rarity = this.layout.rarity, + file_path = this.file_path, + justification = Justification.RIGHT, ), new ScaledTextField( layer = type_line_selected, @@ -230,13 +233,13 @@ var ChilliBaseTemplate = Class({ enable_active_layer_mask(); docref.layers.getByName(LayerNames.HOLLOW_CROWN_SHADOW).visible = true; }, - paste_scryfall_scan: function (reference_layer, file_path, rotate) { + paste_scryfall_scan: function (reference_layer, rotate) { /** * Downloads the card's scryfall scan, pastes it into the document next to the active layer, and frames it to fill * the given reference layer. Can optionally rotate the layer by 90 degrees (useful for planar cards). */ - var layer = insert_scryfall_scan(this.layout.scryfall_scan, file_path); + var layer = insert_scryfall_scan(this.layout.scryfall_scan, this.file_path); if (rotate === true) { layer.rotate(90); } @@ -590,9 +593,11 @@ var ExpeditionTemplate = Class({ text_colour = get_text_layer_colour(name), ), new ExpansionSymbolField( - layer = expansion_symbol, - text_contents = expansion_symbol_character, + layer_group = expansion_symbol, + set_code = this.scryfall.set_code, rarity = this.layout.rarity, + file_path = this.file_path, + justification = Justification.RIGHT, ), new ScaledTextField( layer = type_line, @@ -828,9 +833,11 @@ var IxalanTemplate = Class({ text_colour = get_text_layer_colour(name), ), new ExpansionSymbolField( - layer = expansion_symbol, - text_contents = expansion_symbol_character, + layer_group = expansion_symbol, + set_code = this.scryfall.set_code, rarity = this.layout.rarity, + file_path = this.file_path, + justification = Justification.CENTER, ), new TextField( layer = type_line, @@ -1086,7 +1093,7 @@ var SagaTemplate = Class({ // paste scryfall scan app.activeDocument.activeLayer = app.activeDocument.layers.getByName(LayerNames.TWINS); - this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME), file_path); + this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME)); }, rules_text_and_pt_layers: function (text_and_icons) { var saga_text_group = text_and_icons.layers.getByName("Saga"); @@ -1209,7 +1216,7 @@ var PlaneswalkerTemplate = Class({ // paste scryfall scan app.activeDocument.activeLayer = this.docref.layers.getByName(LayerNames.TEXTBOX); - this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME), file_path); + this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME)); }, enable_frame_layers: function () { // twins and pt box @@ -1317,7 +1324,7 @@ var PlanarTemplate = Class({ // paste scryfall scan app.activeDocument.activeLayer = docref.layers.getByName(LayerNames.TEXTBOX); - this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME), file_path, true); + this.paste_scryfall_scan(app.activeDocument.layers.getByName(LayerNames.SCRYFALL_SCAN_FRAME), true); }, enable_frame_layers: function () { }, }); diff --git a/scripts/text_layers.jsx b/scripts/text_layers.jsx index 91ef653..4822112 100644 --- a/scripts/text_layers.jsx +++ b/scripts/text_layers.jsx @@ -125,11 +125,85 @@ function vertically_nudge_creature_text(layer, reference_layer, top_reference_la /* Class definitions */ +var Field = Class({ + /** + * Interface for a Field class. The system will run the `execute` functions of all Fields in `self.text_layers` when rendering a card. + */ + + constructor: function () { + throw new Error("This Field's constructor is not defined!"); + }, + execute: function () { + throw new Error("This Field's execute function is not defined!") + }, +}); + +var ExpansionSymbolField = Class({ + /** + * A Field which represents a card's expansion symbol. The expansion symbol is retrieved from Scryfall and stored on disk as SVG when the + * card's information is queried, then loaded into the document here. The symbol is sized and positioned according to a reference layer + * (typically right-justified, but centre alignment is also supported), and a 6 px outer stroke is applied. If the card is common, a white + * stroke is applied; otherwise, a black stroke is applied, and a clipping mask for the rarity colour is aligned to the expansion symbol + * and enabled. + */ + + extends_: Field, + constructor: function (layer_group, set_code, rarity, file_path, justification) { + this.layer_group = layer_group; + this.set_code = set_code.toUpperCase(); + if (use_default_expansion_symbol) { + this.set_code = default_icon_name; + } + this.rarity = rarity; + if (rarity === rarity_bonus || rarity === rarity_special) { + this.rarity = rarity_mythic; + } + this.file_path = file_path; + this.justification = justification; + }, + execute: function () { + var expansion_symbol_file = new File(file_path + icon_directory + this.set_code + dot_svg); + var reference_layer = this.layer_group.layers.getByName(LayerNames.EXPANSION_REFERENCE); + app.activeDocument.activeLayer = reference_layer; + var expansion_symbol_layer = paste_file_into_new_layer(expansion_symbol_file); + frame_layer(expansion_symbol_layer, reference_layer); + + // centre justified by default + if (this.justification === Justification.LEFT) { + select_layer_pixels(reference_layer); + align_left(); + clear_selection(); + } else if (this.justification === Justification.RIGHT) { + select_layer_pixels(reference_layer); + align_right(); + clear_selection(); + } + reference_layer.visible = false; + + var stroke_weight = 6; // pixels + app.activeDocument.activeLayer = this.layer_group; + if (this.rarity === rarity_common) { + apply_outer_stroke(stroke_weight, rgb_white()); + } else { + var mask_layer = this.layer_group.parent.layers.getByName(this.rarity); + mask_layer.visible = true; + // ensure the gradient layer is aligned to the expansion symbol + apply_outer_stroke(stroke_weight, rgb_black()); + app.activeDocument.activeLayer = mask_layer; + select_layer_pixels(expansion_symbol_layer); + align_horizontal(); + align_vertical(); + clear_selection(); + } + } +}); + var TextField = Class({ - /* - A generic TextField, which allows you to set a text layer's contents and text colour. - */ + /** + * A generic TextField which allows you to set a text layer's contents and text colour. + */ + extends_: Field, constructor: function (layer, text_contents, text_colour) { this.layer = layer; this.text_contents = ""; @@ -164,37 +238,6 @@ var ScaledTextField = Class({ } }); -var ExpansionSymbolField = Class({ - /** - * A TextField which represents a card's expansion symbol. Expansion symbol layers have a series of clipping masks (uncommon, rare, mythic), - * one of which will need to be enabled according to the card's rarity. A 6 px outer stroke should be applied to the layer as well, white if - * the card is of common rarity and black otherwise. - */ - - extends_: TextField, - constructor: function (layer, text_contents, rarity) { - this.super(layer, text_contents, rgb_black()); - - this.rarity = rarity; - if (rarity === rarity_bonus || rarity === rarity_special) { - this.rarity = rarity_mythic; - } - }, - execute: function () { - this.super(); - - var stroke_weight = 6; // pixels - app.activeDocument.activeLayer = this.layer; - if (this.rarity === rarity_common) { - apply_stroke(stroke_weight, rgb_white()); - } else { - var mask_layer = this.layer.parent.layers.getByName(this.rarity); - mask_layer.visible = true; - apply_stroke(stroke_weight, rgb_black()); - } - } -}) - var BasicFormattedTextField = Class({ /** * A TextField where the contents contain some number of symbols which should be replaced with glyphs from the NDPMTG font. @@ -262,7 +305,7 @@ var FormattedTextField = Class({ this.layer.textItem.justification = Justification.CENTER; } } -}) +}); var FormattedTextArea = Class({ /** @@ -308,4 +351,4 @@ var CreatureFormattedTextArea = Class({ // shift vertically if the text overlaps the PT box vertically_nudge_creature_text(this.layer, this.pt_reference_layer, this.pt_top_reference_layer); } -}) +}); diff --git a/settings.jsx b/settings.jsx index 4b87d26..7fe6704 100644 --- a/settings.jsx +++ b/settings.jsx @@ -1,7 +1,9 @@ #include "scripts/templates.jsx"; -// Expansion symbol - characters copied from Keyrune cheatsheet -var expansion_symbol_character = ""; // Cube +// Expansion symbol settings - if set to true, the system will use `default.svg` as the expansion symbol for all cards. +// If set to false, the system will use the expansion symbol that appears on the printed card. This may not look 100% correct +// when compared to real cards. +var use_default_expansion_symbol = true; // Specify a template to use (if the card's layout is compatible) rather than the default template var specified_template = null;