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)
This entry was posted in Dev, Programming. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *