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.
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:
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.
# 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.
# 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)
I have used your command with Imagemagick 7.0.8-24.
But I used a layered TIF file instead of PSD.
My file has 2 layers.
But this works just fine, if you just want to extrakt one layer from the file and still keep the original size and the convert it to PNG and keeping your transparency.
magick testfile.tif \( -clone 0 -alpha set -channel rgba -evaluate set 0 \) -delete 0,1 +swap -layers merge testfile.png
or
magick testfile.tif \( -clone 0 -alpha transparent \) -delete 0,1 +swap -layers merge testfile.png
There is a topic of this on imagemagick website:
https://imagemagick.org/discourse-server/viewtopic.php?f=1&p=163016#p163016
This works for layered TIF on 7.0.8-24
magick “/path/to/my/testfile.tif” \( -clone 0 -alpha transparent -channel rgba -evaluate set 0 \) -delete 0,1 -background none -compose src-over -layers merge “/path/to/my/testfile.png”