Gimp Trading Card Plugin

by Christopher King

Table Of Contents

TLDR: I made a plugin for GIMP that reads data in 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 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 a example project directory with a GIMP template file on Github here: TCG-Gimp-Plugin


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 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 TCG

Again, 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.
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:
That looks okay, but it doesn't perfectly match the sketch. I cleaned up some of the bumps, darkened the center down and added a few more details to get something a lot closer to our target.
Okay, so with that out of the way, lets get onto the interesting part: the card template.

Designing the Template

I 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 Plugin

Like 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.
        "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",
        "/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", "")



Lets 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 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
            pdb.gimp_message("text layer " + col)
            pdb.gimp_text_layer_set_text(layer, row[col_index])


I 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_image_select_item(timg, 2, layer)


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 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
       = 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])
                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 wouldnt 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 == name:
                return 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 == name:
                return 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.
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 <>

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!