Compressing PNG images in Photoshop with JavaScript and TinyPNG

Updated on

Photoshop's script engine is a flexible alternative to batch automation. We show how to create a script to compress many PNG files with the TinyPNG plugin for Photoshop.

TinyPNG is a web service that compresses PNG images very efficiently by reducing the number of colours in the image. Since the beginning of 2014 it's also available as an Adobe Photoshop plugin.

The plugin supports scripting, which makes it suitable for Photoshop actions. Actions can be used in combination with batch automation to compress a large number of images. An alternative is to write scripts to automate the compression. In this article we describe how to use Photoshop and the TinyPNG plugin with JavaScript.

Creating and running JavaScript in Photoshop

Scripts for Photoshop can be created and edited with a text editor or with Adobe's development environment, called Adobe ExtendScript Toolkit. A script has a .jsx extension and should start with the line #target photoshop.

#target photoshop

alert("Hello world!");

You can run this script by saving it to hello.jsx and then double-clicking on the file. You will be prompted to confirm you want to run it in Photoshop. You can also run it by dragging the file directly to Photoshop. This will work in Mac OS X as well as Windows.

Hello world example

The scripts are written in JavaScript and have access to Photoshop's built-in functionality as well as plugins that support scripting. Adobe has an API reference and documentation about Photoshop Scripting on their website.

Figuring out how to script a Photoshop feature

Most Photoshop features can be scripted, but how to write the correct code isn't always straightforward. By installing Adobe's Scripting Listener plugin we can record the exact code that is necessary to perform a certain operation. You can download it from Adobe's Photoshop Scripting documentation page. Copy it to Photoshop's Plug-ins folder, then restart Photoshop.

Now we execute the actions that we want to use in our script. In this case we want to use the TinyPNG plugin for Photoshop. Therefore we open a PNG file and compress it with TinyPNG. The file locations do not matter, because we can easily change them later. With the Scripting Listener plugin installed, the sequence of steps necessary to these actions will be recorded to a file called ScriptingListenerJS.log on the desktop. The result looks like this:

// =======================================================
var idOpn = charIDToTypeID( "Opn " );
    var desc1 = new ActionDescriptor();
    var idnull = charIDToTypeID( "null" );
    desc1.putPath( idnull, new File( "/Users/rolftimmermans/Desktop/example.png" ) );
executeAction( idOpn, desc1, DialogModes.NO );

// =======================================================
var idExpr = charIDToTypeID( "Expr" );
    var desc2 = new ActionDescriptor();
    var idUsng = charIDToTypeID( "Usng" );
        var desc3 = new ActionDescriptor();
        var idIn = charIDToTypeID( "In  " );
        desc3.putPath( idIn, new File( "/Users/rolftimmermans/Desktop" ) );
    var idtinY = charIDToTypeID( "tinY" );
    desc2.putObject( idUsng, idtinY, desc3 );
executeAction( idExpr, desc2, DialogModes.NO );

We are only interested in the second half. It describes precisely which steps are needed in order to script the TinyPNG plugin. It will contain many calls to charIDToTypeID with arguments consisting of four characters. These four-character codes are the internal identifiers of the Photoshop operations and the settings that we intend to automate.

After recording we immediately remove the Scripting Listener plugin again, because otherwise it would keep recording everything!

Compressing a file with TinyPNG

Armed with our new knowledge about the TinyPNG plugin for Photoshop, we can now write a script to compress an image to an optimized PNG file.

#target photoshop

/* Open the given file, and compress with TinyPNG. */
function compressFile(file) {
  var document = open(file);

  if (document.mode == DocumentMode.INDEXEDCOLOR) {
    document.changeMode(ChangeMode.RGB);
  }

  if (document.bitsPerChannel == BitsPerChannelType.SIXTEEN) {
    convertBitDepth(8);
  }

  var type = charIDToTypeID("tyPN"); /* tyJP for JPEG */
  var percentage = 100;

  var tinypng = new ActionDescriptor();
  tinypng.putPath(charIDToTypeID("In  "), file); /* Overwrite original! */
  tinypng.putEnumerated(charIDToTypeID("FlTy"), charIDToTypeID("tyFT"), type);
  tinypng.putUnitDouble(charIDToTypeID("Scl "), charIDToTypeID("#Prc"), percentage );

  var compress = new ActionDescriptor();
  compress.putObject(charIDToTypeID("Usng"), charIDToTypeID("tinY"), tinypng);
  executeAction(charIDToTypeID("Expr"), compress, DialogModes.NO);

  document.close(SaveOptions.DONOTSAVECHANGES);
}

function convertBitDepth(bitdepth) {
  var id1 = charIDToTypeID("CnvM");
  var convert = new ActionDescriptor();
  var id2 = charIDToTypeID("Dpth");
  convert.putInteger(id2, bitdepth);
  executeAction(id1, convert, DialogModes.NO);
}

compressFile(File.openDialog("Compress file with TinyPNG"));

The function compressFile starts by opening any given image. If the colour mode is indexed we change it to RGB, because the TinyPNG Photoshop plugin does not yet accept files with indexed colours. Similarly we convert 16 bit RGB images to 8 bit RGB.

The TinyPNG plugin is then configured to set the output location. The plugin will compress and overwrite the original PNG file. We don't need the original anymore, so we close the document and discard any changes.

We decide which file to compress by presenting a dialog with File.openDialog. It allows you to select the file that will be compressed.

File.openDialog to choose a file

Compressing all files in a folder and its subfolders

We can enhance the script by asking for a folder and traversing through the entire folder structure, attempting to open and compress each file.

#target photoshop

/* Open the given file, and compress with TinyPNG. */
function compressFile(file) {
  var document = open(file);

  if (document.mode == DocumentMode.INDEXEDCOLOR) {
    document.changeMode(ChangeMode.RGB);
  }

  if (document.bitsPerChannel == BitsPerChannelType.SIXTEEN) {
    convertBitDepth(8);
  }

  var type = charIDToTypeID("tyPN"); /* tyJP for JPEG */
  var percentage = 100;

  var tinypng = new ActionDescriptor();
  tinypng.putPath(charIDToTypeID("In  "), file); /* Overwrite original! */
  tinypng.putEnumerated(charIDToTypeID("FlTy"), charIDToTypeID("tyFT"), type);
  tinypng.putUnitDouble(charIDToTypeID("Scl "), charIDToTypeID("#Prc"), percentage );

  var compress = new ActionDescriptor();
  compress.putObject(charIDToTypeID("Usng"), charIDToTypeID("tinY"), tinypng);
  executeAction(charIDToTypeID("Expr"), compress, DialogModes.NO);

  document.close(SaveOptions.DONOTSAVECHANGES);
}

function convertBitDepth(bitdepth) {
  var id1 = charIDToTypeID("CnvM");
  var convert = new ActionDescriptor();
  var id2 = charIDToTypeID("Dpth");
  convert.putInteger(id2, bitdepth);
  executeAction(id1, convert, DialogModes.NO);
}

/* Recursively compress files in the given folder, overwriting the originals. */
function compressFolder(folder) {
  var children = folder.getFiles();
  for (var i = 0; i < children.length; i++) {
    var child = children[i];
    if (child instanceof Folder) {
      compressFolder(child);
    } else {
      /* Only attempt to compress PNG files. */
      if (child.name.slice(-4).toLowerCase() == ".png") {
        compressFile(child);
      }
    }
  }
}

try {
  compressFolder(Folder.selectDialog("Compress folder with TinyPNG"));
} catch(error) {
  alert("Error while processing: " + error);
}

This example is available as a gist on Github.

The function compressFolder iterates over all files and subfolders in the given folder. It will attempt to compress every PNG file it encounters, and will recursively call itself for each subfolder. We wrap compressFolder with a try/catch block so that we can report any errors.

The call to Folder.selectDialog presents a dialog to select a folder to start from.

Folder.selectDialog to choose folder

After selecting a folder, you'll notice that Photoshop starts opening and compressing all PNG files that are found in the folder and its subfolders.

Summary

Writing JavaScript for Adobe's scripting engine is an interesting and flexible alternative to Photoshop actions and batch automation. We demonstrated how you can record scripts with the Scripting Listener plugin. We used it to create a script that uses TinyPNG to compress all PNG files in a folder and its subfolders.

Scripting is especially useful when the actions that need to be performed depend on the input. In this example we configure the TinyPNG Photoshop plugin to set the output location in order to overwrite the original. When batch automation doesn't suffice anymore, consider using JavaScript!

Update on May 6, 2015

Updated code example to be compatible with new JPEG and PNG plugin version 2.0.

Update on July 21, 2015

Added code that lowers the bit depth for 16 bit RGB images.