GIMP Trading Card Plugin

I made a plugin for GIMP that reads data from a CSV file to automatically populate a template with data and export the rendered files.

I've been a big fan of Magic the Gathering, Yu Gi Oh, and other trading card games ever since I was a kid. Recently I stumbled on to Mark Rosewater's blog where he talks about how they design a set of cards. This got me thinking it could be a fun design challenge to make my own set of trading cards. This post is going to be more about the tool I developed to help expedite the process, but if you're interested in how a TCG set is designed be sure to check out that link. You can find the finished plugin and an example project directory with a GIMP template file on GitHub here: TCG-Gimp-Plugin

Requirements

No 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:

MakePlayingCards Template
  • 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 TCG

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 Back

The 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.

Card Back Sketch
Card Back Work in Progress

Then I started rendering some details. I used a nice little trick to make the card template look professional and not like it came out of the 1980's. Instead of rendering the details like one would for a normal painting, I painted exclusively in black and white so I could use the painting as a bumpmap.

GIMP has a filter that can render the light and shadow for us if we provide it with a height map. The key thing I've found when painting bumpmaps is to start with a gray with 50% value. From there you can use a lighter value to make something closer to you and a darker value to carve into the image. An important thing to remember is that any two matching values are on the same height plane. So applying that filter on our map and adding a few extra details will give us this:

Card Back Version 2
Final Card Back

Designing the Front Template

Text

Let's 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 won't 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])

Color

I made the template with a monochromatic frame and added a color layer so the plugin 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 straightforward would be to use hexadecimal 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)

Images

Here 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 hundreds 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 plugin 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[0], layer.offsets[1], layer.width + extrawidth + layer.offsets[0], layer.height + layer.offsets[1])
        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 Together

Now 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.

If you're looking for some decent resources on how to script a plugin check these out:

Originally I hadn't really considered that grouped layers in GIMP wouldn't come up when searching for their names. I had to modify my search function to recursively look through each group and subgroup for the right layers:

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[1]:
        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

I assigned the plug-in to File/Export TCG files. So if we click that, it'll bring up a dialog where we can specify the parameters to our function:

Plugin Dialog

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 its magic and we will have a directory full of our finished assets.

Closing Thoughts

There is nothing limiting this plugin to just trading cards. I could see this being used for any case where there is a lot of data that you want rendered in a specific format. You're welcome to use and modify the plugin if you see fit. Again, you can find it here on my GitHub. I'd love to see what you end up using this for, so feel free to send me a message!

Share this article:

Table of Contents