Skip to main content

Manipulating Photoshop layers using JavaScript

Front-end Development

In a previous article, I wrote about using JavaScript to read image information from a Photoshop document and output it as text. Today, I'm going to talk about the opposite: reading in information from a text file and using it to manipulate a Photoshop document.

The typical use case for this is needing to produce a large number of similar images. Sometimes related images are required that differ in details such as the text typed in some layers, the visibility of layers, colors of filters applied to layers, and so forth. It is usually possible to manage these kinds of variations using Layer Comps in Photoshop, but if the details may change in the future, keeping all the comps up to date can be laborious. Instead, we can keep track of the data in a spreadsheet and use that to produce the images when needed.

For example, consider a Photoshop file containing the assets for producing tiles for a board game. Each tile will require a different number, a custom title, and some layers to be turned on and off. We will want to produce a PNG image corresponding to each tile.

The CSV data looks like this:

Tile,#,Name,Type,2p,3p,4p,5p
-1,5,Put a tack on the teacher’s chair,-,1,2,3,4
+1,6,Put an apple on the teacher’s desk,+,1,2,3,4
+1,7,Donate to charity,+,1,1,1,1
...

Our script will begin by reading in data from the CSV file and converting it into a JavaScript data structure we can work with.

var data = [];
var dataFile = new File(app.activeDocument.path + '/data.csv');
dataFile.open('r');
dataFile.readln(); // Skip first line
while (!dataFile.eof) {
  var dataFileLine = dataFile.readln();
  var dataFilePieces = dataFileLine.split(',');
  data.push({
    art: dataFilePieces[0],
    tileNumber: dataFilePieces[1],
    tileName: dataFilePieces[2],
    spaces: dataFilePieces[7],
    permanent: dataFilePieces[3] == 'Permanent'
  });
}
dataFile.close();

This routine assumes that the data file is called data.csv. We skip the first line of the file, which contains the column headers, and read the rest of the lines one by one. Each line becomes an object which is pushed onto our data array. Converting the lines from arrays to objects certainly isn't necessary, but it will make the rest of our code much easier to read.

The rest of our code loops through the data lines one by one:

for (var tileIndex = 0; tileIndex < data.length; tileIndex++) {
  var tileData = data[tileIndex];
  // Our code goes here...
}

Inside the loop, first we will update the text of the number in the upper left corner of the tile.

app.activeDocument.artLayers.getByName('Tile number').textItem.contents = tileData.tileNumber;

We're going to use the .getByName() method a lot. It searches through a specific set of layers for one with a given name. There's no way (that I've found) to easily search the entire document for a layer of a given name, so putting the layers to be switched on and off at the top level rather than in a group will make our life a little easier.

Next up, we'll handle updating the tile name.

app.activeDocument.artLayers.getByName('Tile name').textItem.contents = (tileData.tileName ? tileData.tileName : ' ');

This is very similar to the last line, but in this case we need to handle the possibility of a tile not having a name (since not all of them do in our CSV data). Photoshop doesn't react well to deleting all of the text from a text layer, so we can work around this by using a single space character instead of nothing at all in these cases.

Our next trick will be to turn layers on. One of the columns in our CSV file contains the name of a layer to make visible. Toggling visibility is pretty easy:

app.activeDocument.layerSets.getByName(tileData.art).visible = true;

We can also, of course, construct a layer name programmatically from our data rather than using some data verbatim as the layer name.

var spaceSet = tileData.spaces + ' Space' + (tileData.spaces == 1 ? '' : 's');
app.activeDocument.layerSets.getByName(spaceSet).visible = true;

app.activeDocument.artLayers.getByName('Permanent').visible = tileData.permanent;

Now let's see how to output the result of our work. We will get the best results by using the "Save for Web" feature in Photoshop. Using this feature from JavaScript requires us to set up the dialog options first, then call a method of the document object to actually do the saving.

file = new File(app.activeDocument.path + '/' + tileData.tileNumber + '.png');
opts = new ExportOptionsSaveForWeb();
opts.format = SaveDocumentType.PNG;
opts.PNG8 = false;
opts.quality = 100;
app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, opts);

Finally, we need to hide the layers that were shown, so that the next tile starts with them not visible.

app.activeDocument.layerSets.getByName(tileData.art).visible = false;
app.activeDocument.layerSets.getByName(spaceSet).visible = false;

That's all! We can now use "File -> Scripts -> Browse" to locate our script and run it, and the tile PNG files are generated for us.

The full JavaScript code looks like this:

var data = [];
var dataFile = new File(app.activeDocument.path + '/data.csv');
dataFile.open('r');
dataFile.readln(); // Skip first line
while (!dataFile.eof) {
  var dataFileLine = dataFile.readln();
  var dataFilePieces = dataFileLine.split(',');
  data.push({
    art: dataFilePieces[0],
    tileNumber: dataFilePieces[1],
    tileName: dataFilePieces[2],
    spaces: dataFilePieces[7],
    permanent: dataFilePieces[3] == 'Permanent'
  });
}
dataFile.close();


for (var tileIndex = 0; tileIndex < data.length; tileIndex++) {
  var tileData = data[tileIndex];

  app.activeDocument.artLayers.getByName('Tile number').textItem.contents = tileData.tileNumber;

  app.activeDocument.artLayers.getByName('Tile name').textItem.contents = (tileData.tileName ? tileData.tileName : ' ');

  app.activeDocument.layerSets.getByName(tileData.art).visible = true;
  
  var spaceSet = tileData.spaces + ' Space' + (tileData.spaces == 1 ? '' : 's');
  app.activeDocument.layerSets.getByName(spaceSet).visible = true;
  
  app.activeDocument.artLayers.getByName('Permanent').visible = tileData.permanent;
  
  file = new File(app.activeDocument.path + '/' + tileData.tileNumber + '.png');
  opts = new ExportOptionsSaveForWeb();
  opts.format = SaveDocumentType.PNG;
  opts.PNG8 = false;
  opts.quality = 100;
  app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, opts);

  app.activeDocument.layerSets.getByName(tileData.art).visible = false;
  app.activeDocument.layerSets.getByName(spaceSet).visible = false;
}