RequirementsNo project can get done without a list of requirements to check off, so let's talk a little about those first. One of the best tools a designer has is an Excel spreadsheet where you can analyze and quickly tweak values as you hone in on making the game balanced. As for the artist, Photoshop is the go to professional tool, but I don’t like paying a monthly fee, so GNU Image Manipulation Program (GIMP) is the next best thing. So now that we figured out how we want to go about making some cards, the question is: “How do we make them work together so we don't have to manually enter data for hundreds of images?” GIMP lets you create custom plugins in python that extend and automate its functionality, which is exactly what I’m trying to do here. I’m going to need the plug-in to parse through my CSV line by line and match the card data to a specific layer. The data itself might be some mix of text, images, or colors. Then finally the plug-in should export the cards into a folder so I can upload them to MakePlayingCards.com. MPC is nice enough to give us a template and some printing requirements to work off of
Image resolution: Minimum 300 dpi
Bleeding: Allow 1/8" (approx 36 pixels based on a 300dpi image) for bleeding and a further 1/8" for safe area margin inside each side.
This video shows what settings we are going to need to print cards of similar quality to MTG. Speaking of MTG, their alpha set consisted of 295 cards so that's the ballpark we should expect for number of lines in our CSV files.
Designing the TCGAgain, I’m going to skip this for now as it's out of scope of this post, but all we really need to know from this is what data is going to be present on the cards. For now we can assume each card will have a name, type, color, power, description, flavor, and a unique piece of art.
Designing the BackThe cards will have a unique front face and common back, so let's load that template MPC gave us into two separate GIMP projects. The card back isn’t going to have anything to do with the plugin because it’s the same on all the cards, so I’ll just briefly mention it here and then get to the more interesting part. For the card back I wanted something that was inspired by MTG and Yu Gi Oh’s iconic cards while at the same time being able to stand on its own. I started out making sketches until I found one that I liked.
Designing the TemplateI know that I’m going to relate the CSV columns to project layers by matching the column names to the names of the layers. So I have to be careful to use the exact same names. With that one consideration in mind, I can go ahead and create a separate layer for each column in the CSV file. I’m creating the card frame in the same way as the card back by using a bump map. But this time instead of having a colored layer as a target of the bumpmap filter, I’m keeping the color in a separate layer.
Setting up the PluginLike I said earlier, GIMP uses python to handle it’s plug-ins. GIMP has a specific folder where all of the installed plugins exist. You can find it by going to Edit > Preferences > Folders > Plug-ins. Any python files in this folder will get loaded in when GIMP starts. For the plug-in to actually load, you have to register a python function that will get called when you click the menu item. Your function uses the gimpfu library and the procedure database object to reference other GIMP functions or the image’s data.
register( "python_fu_tcg", "Loads in a CSV file and a directory of art assets and exports a playing card using this template", "Loads in a CSV file and a directory of art assets and exports a playing card using this template", "Christopher King", "Christopher King", "2022", "/File/Export TCG files", "RGB*, GRAY*", [ #get the project directory (PF_FILE, "data_dir", "Project Directory", "/"), #get the output directory (PF_FILE, "data_out", "Output Directory", "/"), #get the csv file (PF_FILE, "data_csv", "Card Data CSV", "") ], , plugin_main) main()
TextLets start with the text. We can use a neat trick to add dynamic text shadows which is usually not supported in GIMP by creating a duplicate text layer under the current text and offset it by a few pixels. In order to keep with our rule of matching names to CSV columns, we can either add a duplicate column to the CSV and just set the value equal to the row on the right, or code it into the plugin. For the sake of efficiency and just in case we ever want to do something different with that shadow layer, I think it'd be best to add in a column to the CSV. We wont have to type the name twice and we can even hide that column to reduce the chance of user error. The template is also going to need a nice looking font. Taking some inspiration from looking at a magic card, we can see they use a nice serif font, so why not follow suit. I grabbed the free font Oldstyle by HPLHS Prop Fonts from Dafont.com. A good way to pick one that will look nice on a card is to scale(ctrl+scroll) the site way down.
#it is a text layer else: pdb.gimp_message("text layer " + col) pdb.gimp_text_layer_set_text(layer, row[col_index])
ColorI made the template with a monochromatic frame and added a color layer so the plug in can just recolor that layer to the desired hue. There's a few different ways we could handle this in the plugin, but I think the most straight forward would be to use hexideciamal color format, and then the plugin can add the rule that any cell value starting with a '#' will be read as a color for that layer
#if the value in the layer is a color, recolor the layer elif row[col_index] != None and row[col_index].find("#") != -1: pdb.gimp_message("found a color for " + col) pdb.gimp_selection_none(timg) pdb.gimp_image_select_item(timg, 2, layer) pdb.gimp_context_set_foreground(row[col_index]) pdb.gimp_drawable_edit_fill(layer,FILL_FOREGROUND) pdb.gimp_selection_none(timg)
ImagesHere is the fun part! I want to allow for an arbitrary number of images to be added to an equally arbitrary number of layers. everything could easily become disorganized when working with hundereds of image files, so we should use some directory structure to keep things organized. All of our background art will go in a folder named "background_art" and then we can use that name as both the CSV column name and the template layer name. Any values in that column should then match up to the image file names in that directory. And along those lines, we can just add a check in the plug in to look for directories with a matching name and if they exist, we will know that the column is for an image asset instead of text.
#if there exists a folder in the directory with the same name as the column, the value is a file name if os.path.exists(path): pdb.gimp_message("found an asset folder for " + col) img_path = os.path.join(path, row[col_index]) new_asset = pdb.gimp_file_load_layer(timg, img_path) if new_asset != None: #add the new image, scaled to the right height and in the correct position and merge it down new_asset.name = col opacity_before = layer.opacity timg.add_layer(new_asset, -1) scalefactor = new_asset.height / layer.height extrawidth = ((new_asset.width / scalefactor) - layer.width) / 2 pdb.gimp_item_transform_scale(new_asset, 0 - extrawidth + layer.offsets, layer.offsets, layer.width + extrawidth + layer.offsets, layer.height + layer.offsets) pdb.gimp_layer_resize_to_image_size(new_asset) layer = pdb.gimp_image_merge_down(timg, new_asset, 2) layer.opacity = opacity_before
Putting it all TogetherNow that the template is set, I can start working on the plugin. First step is to set up a sample directory and create some test data so we can make sure that the plugin will handle all the different cases that we throw at it. <
def find_layer_by_name (image, name): for layer in image.layers: if layer.name == name: return layer if(pdb.gimp_item_is_group(layer)): potential = find_layer_name_in_group(layer, name) if(potential != None): return potential return None def find_layer_name_in_group(layerGroup, name): gr = layerGroup gr_items = pdb.gimp_item_get_children(gr) for index in gr_items: item = gimp.Item.from_id(index) if item.name == name: return item elif(pdb.gimp_item_is_group(item)): potential = find_layer_name_in_group(item, name) if(potential != None): return potential return None
We need to specify a directory to search for assets, the CSV file with all the data, and an output directory for the finished files. Once we provide those the plugin will work it’s magic and we will have a directory full of our finished assets <