How to Extract Layers from a Photoshop File with ImageMagick and Python

For a project that I’m working on, I need a way to extract each layer of a Photoshop PSD file and have them written out as separate PNG image files. To do this, my first thought was to use ImageMagick. ImageMagick is a graphics library used to manipulate images and convert them to different graphic file types, among many other things. I’ve used ImageMagick before, but I don’t use it enough to remember all of the syntax by heart, so I had to do a lot of google searches to compile this solution together. I could have rolled my own solution, but time is the enemy, so using any sort of libraries that saves you hours, days, or weeks, is an obvious win.

Requirements

The requirements for this system are pretty simple. A Python script can be run from the command line, given the path to a Photoshop PSD file. After the script is complete, the folder containing the PSD file now also contains all PNG files for each layer in the PSD file.

Overview of Extracting Layers from Photoshop with ImageMagick and Python

Overview of Extracting Layers from Photoshop with ImageMagick and Python

Pretty simple, right? Not quite.

As I was implementing this solution, I discovered some information and processing techniques that aren’t immediately available through ImageMagick’s commands.

The most basic command to extract layers from a PSD file is straightforward enough:

convert <filename>.psd[1] <extracted-filename>.png

Where, the number 1 is the index of a layer in the PSD file. But here are the problems I ran into:

Irregular Image Sizes for Alpha Layers

If the layer is an alpha layer, all of the transparent pixels on the edge of the image on that layer are not accounted for in the dimension of the output file, thereby resulting in images with varying dimensions. For example:

Irregular Alpha Images when Extracting Layers from Photoshop with ImageMagick

Irregular Alpha Images when Extracting Layers from Photoshop with ImageMagick

This is a no-go for my purposes, because I need to later re-composite the layers, and if each output layer is a different size, they can’t be easily merged back together.

Unable to Export by Layer Name

This is another feature that I was expecting to use in ImageMagick that isn’t actually there.

To export a layer, you have to use the index rather than the name of the layer. I suppose this change isn’t absolutely necessary as a requirement for everyone, but I feel that it’s a lot more convenient to specify the name of a layer that’s easily cross-referenced by human eyes in the Photoshop file, as opposed to having to count what index is required in the layer stack. Not only that, you can do other selection tricks, like filter layers by specific names, which is actually the real purpose for supporting this.

So those are the two major problems I ran into, and that’s the reason why I chose Python for this solution.The solution below could very well have been done using batch scripts, or some other scripting language, but I’m a Python user. There are multiple steps to get this working. In the Implementation section, I’ll spill out all the details.

Setup

Before I dive into the details, I want to mention that this article already assumes that you have your environment set up to run these operations. If you’re sure that it’s set up, feel free to skip ahead to the Implementation section.

Photoshop – I use Photoshop Elements, but you’ll obviously need some program to author PSD files for processing.

ImageMagick – I initially started implementing this with version 6.9.3, but have since upgraded to 7.0.1. I was taken aback a bit, since by default, versions 7.0.+ have marked the convert command as legacy, and the solution in this blog post uses the convert command. If you are using this version, be sure to check the option to “Install legacy utilities (e.g. convert)” before installing it. Older versions do not have this problem.

Python – I’m using the 2 series, not the 3 series for this solution. Be sure that you can run Python files directly from anywhere in the file system, without having to specify python.exe in front of it. For me, I had to do 'regedit', and modify the value in HKEY_CLASSES_ROOT/py_auto_file/shell/open/command to:

"C:\Python27\python.exe" "%1" %*

Moving along…

Implementation

Fixing the Alpha Image Size Problem

Let’s solve the problem with the irregular alpha image sizes first. I stumbled across the answer on this thread here. But I’ll explain it a little further here, as the syntax is a little strange.

convert <filename>.psd[0] <filename>.psd[2] ( -clone 0 -alpha transparent ) -swap 0 +delete -coalesce -compose src-over -composite <extracted-filename>.png

The first two parameters are the first two image files in the “sequence” for ImageMagick to process. The index (i.e. [0] and [2]) indicates the layer to process. Layer 0 is not really one of the layers in the PSD, but it’s actually the flattened image of all layers in the PSD. Layer 2 is the second layer in the PSD, for this example.

The part of the command in parentheses are options processed as a unit, before moving on to the rest of the options. It creates a clone of the first image in the sequence, and then wipes it clear as an alpha image that’s completely transparent. This new image is tacked on as the third image in the sequence.

The next option (-swap) swaps the last image with the first image in the sequence, and the +delete option deletes the last image (which used to be the first image) because we no longer need it.

I’m not actually sure why -coalesce is needed here, but -compose src-over sets up the image for alpha compositing such that the overlaying image is blended according to the transparency in that layer. The -composite option then performs the compositing of the two images, and outputs the file as <extracted-filename>.png.

Whew! That took a lot to explain, but I hope it at least gives an idea of how ImageMagick is processing the layers in the PSD to produce the resulting images.

I realize that there may be other solutions to this problem, but this is the one I found that actually worked for me. If you have a solution that is more elegant, feel free to ping me, and I can add it here.

Exporting Layers by Name

We handle exporting PSD layers by name in two parts. The first part is getting the layer names, and the second part is using those names. All of the code for this part uses Python.

Part I – Getting the Layer Names

Here is the file in its entirety. We’ll discuss the major points about the code after you take a gander.

Download

#  PSDLayerInfo

# Copyright (c) 2016 Under the Weather, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies
# or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os
import sys
import subprocess
import string


class PSDLayerInfo:

    def __init__(self):
        self.layer_names = []

    def process(self, argv):

        if len(argv) < 2:
            print('No file specified')
            return;
        
        inputFile = argv[1]
        # imagemagick command to print out information about the input file
        identify_cmd = 'identify -verbose ' + inputFile

        # check_output runs the specified command, and writes the result string to the var
        psdInfo = subprocess.check_output(identify_cmd, shell=True)

        # copy the info to be consumed
        data = psdInfo
        while data:
            data = self.find_layer_name(data)

        for name in self.layer_names:
            print(name)

    # partitions the string based on the "label: " string
    # collects the names in layer_names
    def find_layer_name(self, inputStr):
        parts = inputStr.partition("label: ")
        lastPart = parts[2]
        if lastPart:
            index = lastPart.find('\n')
            if (index is not -1):
                self.layer_names.append(lastPart[:index])

        return lastPart
    
def main():
    argv = sys.argv
    layerInfo = PSDLayerInfo()
    layerInfo.process(argv)
    

if __name__ == "__main__":
    main()

So, this script accepts the name of a PSD file as a parameter.

The first notable item is this line:

identify_cmd = 'identify -verbose ' + inputFile

identify is an ImageMagick command, and we are using it here to get some information about the PSD file; specifically, the layer names.

psdInfo = subprocess.check_output(identify_cmd, shell=True)

subprocess.check_output() is a Python function that runs an external program and pipes the output from that function into a byte array, psdInfo.

psdInfo contains tons of information about the PSD file. The function find_layer_name() takes that string, iteratively parses it, and collects all the layer names identified by every occurrence of the "label: " string in psdInfo.

def find_layer_name(self, inputStr):
   parts = inputStr.partition("label: ")
   lastPart = parts[2]
   if lastPart:
      index = lastPart.find('\n')
      if (index is not -1):
         self.layer_names.append(lastPart[:index])

   return lastPart

The partition() function separates the input string and outputs a 3-tuple as 3 string objects, the delimiter "label: " being the second string object. Since we're only interested in the stuff after the "label: " delimiter, we always look at parts[2], being the last part of the string.

Once we have the layer names, we simply print them to the output.

Part II - Using the Layer Names

So, now that we have a way to collect all the layer names from the PSD file, we take those layer names, and export each layer individually, using the convert command that we used to solve the irregular alpha image size problem above.

Download

#  PSDLayerExporter

# Copyright (c) 2016 Under the Weather, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies
# or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os
import sys
import subprocess
import string

class PSDLayerExporter:
    def process(self, argv):
        print("Layer exporter")

        if len(argv) < 2:
            print('No file specified.')
            return

        self.inputFile = argv[1]

        out = subprocess.check_output('PSDLayerInfo.py ' + self.inputFile, shell=True)

        layers = out.split('\n')

        index = 0

        for layer in layers:
            index += 1   
            if layer and (layer.find("base") is not -1 or layer.find("detail") is not -1):
                print("layer: " + layer)
                self.export_layer(index, layer)

    def export_layer(self, psdIndex, layer_name):

        extractedFilename = ""
        extIndex = self.inputFile.rfind(".psd")
        if extIndex is not -1:
            extractedFilename = self.inputFile[:extIndex]

        extractedFilename += "_" + layer_name + ".png"
        cmd = self.inputFile + "[0] " + self.inputFile + "[" + str(psdIndex) + "] ( -clone 0 -alpha transparent ) -swap 0 +delete -coalesce -compose src-over -composite " + extractedFilename;

        commandStr = 'convert ' + cmd

        subprocess.call(commandStr, shell=True)
        
def main():
    argv = sys.argv
    layer_exporter = PSDLayerExporter()
    layer_exporter.process(argv)
    

if __name__ == "__main__":
    main()

For the most part, this file uses common Python syntax and methodology, so I'll again, just point out the major points of interest.

out = subprocess.check_output('PSDLayerInfo.py ' + self.inputFile, shell=True)

Oh look, it's our old friend subprocess.check_output()! We used this function in the other script, to get the output from the ImageMagick identify command. This time, however, we're using it on the PSDLayerInfo.py script that we just discussed above! And this time, we know that the output will be a endline-separated string of all the layer names in the PSD file. Handy!

def export_layer(self, psdIndex, layer_name):

    extractedFilename = ""
    extIndex = self.inputFile.rfind(".psd")
    if extIndex is not -1:
        extractedFilename = self.inputFile[:extIndex]

    extractedFilename += "_" + layer_name + ".png"
    cmd = self.inputFile + "[0] " + self.inputFile + "[" + str(psdIndex) + "] ( -clone 0 -alpha transparent ) -swap 0 +delete -coalesce -compose src-over -composite " + extractedFilename;

    commandStr = 'convert ' + cmd

    subprocess.call(commandStr, shell=True)

export_layer() does most of the work in this file. The first block simply takes the PSD's filename, and modifies it to append the layer name and PNG extension, resulting in the extractedFilename. With that, we call the convert function as mentioned above. Since we don't need any output from this operation, we just call subprocess.call().

 for layer in layers:
     index += 1 
     if layer and (layer.find("base") is not -1 or layer.find("detail") is not -1):
         print("layer: " + layer)
         self.export_layer(index, layer)

Last thing to note here, is that we are looping over all layer names, but only exporting the layers with either "base" or "detail" in their name. This is what I mentioned earlier, about being able to filter out exported layers based on the names of the layers in the PSD. It's not necessary, especially if you know absolutely how many layers and in what order you want your PSD asset files to be in, but it definitely makes it more convenient.

Conclusion

So that wraps it up! Automated PSD layer extraction from now until.... forever! Or more realistically, until Python, ImageMagick, and/or the PSD file format are deprecated, which is probably not any time soon, or even within the next decade or so. But if that is ever the case, well, hopefully I'll have another solution by then. Heck, there might already be another solution that's more elegant and runs faster now, but I do not yet know about it. For now, this works for me. Until next time...

Make it fun!

 

 

  • Adobe Photoshop Elements 9 (for authoring PSDs)
  • Python 2.7.10
  • ImageMagick 7.0.1 (with "convert" command installed)
Posted in Dev, Programming | 2 Comments

Number Crunchers Now Available on iOS

That’s right! Number Crunchers, which was only available for Android for the past several months is now available for the iPhone and iPad!

Play a classic numbers game with a new look and feel! Get it here on the App Store today.

iOS Download

muncher_apple

 

Posted in Announcements | Leave a comment

Horde Rush Now Available On iOS

I am pleased to announce that Horde Rush is now available on iOS. That’s right! Check it out on the App Store here. Play the game on your iPhone or iPad today!

Posted in Announcements | Leave a comment

Ramping up to Take on 2016

Happy 2016! I’ve been off the grid since before Star Wars released. It wasn’t because of Star Wars though, as I had just seen the movie this past weekend. I was keeping busy with everything Holiday celebrations, but mainly have been trying to kick an illness that’s been affecting my productivity.

As for Horde Rush,  I will be submitting the iOS build to Apple sometime soon, but it depends on the results of my internal beta test.

I’ve also been researching tech and prototyping my next project and I’m really excited about it. That’s not to say that I’m leaving my previous projects to rot. Number Crunchers seems to be building up an audience, so it’s only right that I update it with some new stuff. And I still have plenty of future plans to diversify the gameplay in Horde Rush.

Anyway, I was hoping my first post of 2016 would be more informative or educational for the game development community, but it’s been far too long since I’ve updated the blog. All I can say for now is that I have lots of plans for Under the Weather this year, and I’m going to execute these plans to the best of my abilities.

Until next time,

Make it fun!

 

Posted in Thoughts | Leave a comment

Horde Rush 1.02 Update

I’ve updated Horde Rush with a couple of new features and bug fixes!

The biggest difference in this release is the addition of this bad boy:
squash_head_color

This is Squash. Yeah, he’s 48 weeks early for Halloween, but he can throw a pumpkin at you from a distance, so take him down as soon as you can with your arrows. If you can’t, then he’ll try to attack you up close.

I’ve also improved the Shield mechanics. I admit it was pretty difficult to use before. But now, when you shield the Hero from attacks, monsters get stunned for a short time. Not only that, the special meter charges whenever you ward off monster attacks with your shield.

I also fixed a few other annoying bugs, including the bow special attack, which now attacks your closest enemies first!

See the full list of fixes and additions in the release notes here:
http://horderush.com/index.php/release-notes/

Posted in Announcements | Leave a comment

A Couple of Weeks After Launch

It’s been a couple of weeks since Horde Rush has released on the Google Play Store, and I’ve gotten some really good feedback from players. I’m glad to know that most of the feedback is positive, and I’m also glad that whatever negative feedback I’ve gotten, it has been generally constructive, and a lot of it is actionable.

While I’m trying to figure out how best to promote my latest game, I’ve been keeping busy with a few more features and fixes to the gameplay based on the feedback I’ve received, and I’m also working on the iOS port! Yay!

So back to that “promoting” thing… It’s rough. I knew it was going to require a tremendous amount of effort to generate awareness for the game. And I know that many indie game developers fall flat at this point as well. So, I’m still working at it, and my hope is that eventually, a lot more people will enjoy Horde Rush.

Posted in Dev, Thoughts | Leave a comment

Horde Rush is Here!

feature_graphic

I’m very excited that Horde Rush is now available on the Google Play Store. Play it now, if you haven’t already!

Anyway, anyone who has been occasionally checking out this blog will have noticed that the website was down for the last few days. I want to sincerely apologize about this. I’ll write about the outage in a future post, but for now, I’m just glad it’s back up, because now that the game’s out, I would like to talk more about it. Until then, have fun in Horde Rush!

 

Posted in Announcements | Leave a comment

The Horde Rush is Coming!

I’m coming to the final stages of development on my next game, Horde Rush!

title

I’ve been busy with bug fixes and adding some more content, so more detailed updates have been on hold for a couple of weeks, while I prepare for release.

I’m looking to release the game within a week or two, so keep an eye out for it!

Posted in Announcements | Leave a comment

Some New Monsters in Blender

I added a couple of new monsters to my game: a skeleton guy and a werewolf character.

e02_wire_front  e02_wire_perspe02_texturede02_w_e01_walk_anim

I originally thought that modeling and rigging the skeleton would be easier than modeling a skinned mesh, it turned out to be a lot more difficult. I modeled each bone with the intention of simply parenting each rigid bone model to the armature’s bone. I hoped to eventually have the skeleton break apart into its component bones when the hero character strikes it. Unfortunately, at least for this first pass, it was proving too time-consuming to do this way, and this means that the underlying code base would require some special-case implementation to support monsters breaking apart. For the time being, all bones are combined into a single mesh, and thereby skinned to the armature. As far as the texture goes, I know it looks pretty horrible as it is. It’s just a temporary test texture, and I’ll be improving that as I get closer to launch.

The werewolf, on the other hand, was very quick to model, especially with a model sheet that exposed a lot of the detail and dimension for me.

e03_mesh_fronte03_mesh_side

e03_mesh_persp_handse03_mesh_ortho

e03_mesh_persp_back_handse03_mesh_persp_jaw_and_taile03_base_tex

You’ll notice that in the first image, the model sheet drawing isn’t exactly symmetrical. I just worked my way around this and did my best to estimate the vertex positioning. The lower jaw is a separate model that is attached to the chest bone. This way, I can animate the werewolf biting the hero character, because the neck bone will be rotated away from the chest bone. I could have added an additional bone to the armature specifically for the lower jaw, but again, this would require special-case code that I didn’t intend to implement. I’m pretty happy with this model, it’s design, vertex and face count, and how easy it was to build and animate. It also looks like the most menacing character in the game so far.

So, my game is coming along. I’m running into numerous road blocks on the way, but that’s game development for you. I’m hoping to wrap up around mid October, so I’m excited, a little anxious, and just working on the project whenever I can.

Good luck on your own projects, and as always,…

Make it fun!

 

  • Blender 2.74
Posted in Art, Dev | Leave a comment

More Game Progress

Here’s a video of some progress on gameplay from a few weeks back. It’s gettin’ there, slowly but surely!

Posted in Art, Design, Dev, Programming | Leave a comment