File.execute Tutorial

Documentation, Reference material and Tutorials for Photoshop Scripting

Moderator: PS-Moderators

File.execute Tutorial

Postby xbytor » Tue Dec 06, 2005 7:46 pm

Introduction

This document describes techniques used in executing external programs from within Photoshop. The examples used will range from the simple to the complex with some useful stuff mixed in. This will be based on WinXP using .bat files. If there is interest, I can produce a version of this document explaining support for WinXP-cygwin-bash and OSX-bash.

The applications that will be invoked include Irfan, 7Zip, and (naturally) Photoshop. All are assumed to be installed in C:\Program Files. Irfan and 7Zip were selected because of their utility and the fact that they have a nice command line interface which is essential for our purposes.


The Basics

Photoshop provides a rudimentary facility for executing external applications in the form of the File.execute method.

If you wanted to run Irfan from PSJS, you would do the following.
Code: Select all
  var iview = new File("/c/Program Files/IrfanView3.91/i_view32.exe");
  iview.execute();


As soon as the Irfan is launched, File.execute returns. Ifran continues running separately from Photoshop and the calling script. File.execute does not wait for Irfan to finish. I'm hammering this point home because it is not obvious and will become very important later.


Passing Parameters

So far, so good. Now, let's try to do something useful. Irfan has a reasonable slideshow facility. From the command line, it would be invoked like this:
Code: Select all
C:\>"c:\Program Files\IrfanView3.91\i_view32.exe" /slideshow=h:\pics /closeslideshow


Now we have a problem. The File.execute method doesn't take any parameters. To get around this problem, we introduce a layer of indirection in the form of a batch file.
Code: Select all
  var bat = new File(Folder.temp + "/iview.bat");
  bat.open("w");
  bat.writeln("\"c:\\Program Files\\IrfanView3.91\\i_view32.exe\" /slideshow=h:\\pics /closeslideshow");
  bat.close();
  bat.execute();


Note the use of the quotes. Pathnames can have spaces in them. But they can confuse cmd.exe. Putting quotes around pathnames typically addresses the problem.


Making it Clean

We now have a chunk of code that can launch the Irfan's slideshow viewer from within PSJS. It works, but I'd rather not write that much code each time. Besides, we don't always want it to run on the same directory. So, let's rework the code a bit so that it works for anything that we might want to do with Irfan.
Code: Select all
Irfan = function(){};

Irfan.app = new File("/c/Program Files/IrfanView3.91/i_view32.exe");

Irfan.execute = function(args) {
  var str;
  str = '\"' + Irfan.app.fsName + '\"'; // the app

  if (args) {
    // if a file or folder was passed in, convert it to an array of one
    if (args instanceof File || args instanceof Folder) {
      args = ['\"' + args + '\"'];
    }

    // if its an array, we need to appened it to the command
    if (args.constructor == Array) {
      for (var i = 0; i < args.length; i++) {
        str += ' ' + args[i];
      }
    } else {
      // if its something else (like a prebuilt command string), just append it.
      str += ' ' + args.toString();
    }
  }

  var bat = new File(Folder.temp + "/iview.bat");
  bat.open("w");
  bat.writeln(str);
  bat.writeln("del \"" + bat.fsName + "\" > NUL");
  bat.close();
  bat.execute();   
};


We did several things here. We:
- Defined a class/namespace for the program
- Defined a file variable for the program
- Defined an 'execute' method for the class that takes and argument list. This method deals with the (optional) argument list, constructs the command string to execute, writes the command string to a batch file, and executes the batch file.
- If the argument to execute was a single file or folder object, we wrapped it in quotes in case it has embedded spaces.
- We have the batch file delete itself for cleanup purposes.

With this in place, we can write this code to execute an Irfan slideshow on a specific directory:
Code: Select all
Irfan.slideshow = function(folder) {
  Irfan.execute(["/slideshow=\"" + folder.fsName + '\"', "/closeslideshow"]);
};

Irfan.slideshow(new Folder("/h/pics"));



Blocking
There are many cases where you will want to block until the program you've called has terminated. For instance, if you are adding files to a zip archive before deletion, you need to wait until the zip processing is finished before deleting the files.

The mechanism described below relies on $.sleep method being available. There are ways around this missing function in CS and PS7, but I don't recommend either of them.

The zip archive program I use in this situation is 7Zip. It's fast, has very good compression, it's free, and the source code is available. All are important in my book.

To implement a solution for this problem, I could just copy the Irfan implementation and replace where necessary. Instead, however, I will refactor the Irfan implementation and pull out what would be in common with 7Zip.

Code: Select all
//
// Exec
//
Exec = function(app){
  this.app = app;         // A File object to the application
  this.tmp = Folder.temp; // You can use a different folder here
};
Exec.prototype.execute = function(args) {
  var str;
  str = '\"' + this.app.fsName + '\"'; // the app

  if (args) {
    // if a file or folder was passed in, convert it to an array of one
    if (args instanceof File || args instanceof Folder) {
      args = ['\"' + args + '\"'];
    }

    // if its an array, we need to appened it to the command
    if (args.constructor == Array) {
      for (var i = 0; i < args.length; i++) {
        str += ' ' + args[i];
      }
    } else {
      // if its something else (like a prebuilt command string), just append it.
      str += ' ' + args.toString();
    }
  }

  var bat = new File(Folder.temp + '/' + this.app.name + ".bat");
  bat.open("w");
  bat.writeln(str);
  bat.writeln("del \"" + bat.fsName + "\" > NUL");
  bat.close();
  bat.execute();   
};

//
// Irfan
//
Irfan = new Exec(new File("/c/Program Files/IrfanView3.91/i_view32.exe"));
Irfan.slideshow = function(folder) {
  var args = ["/slideshow=\"" + folder.fsName + '\"', "/closeslideshow"];
  this.execute(args);
};

//
// SevenZip
//
SevenZip = function(new File("/c/Program Files/7-Zip/7z.exe"));
SevenZip.archive = function(zipFile, filelist) {
  var args = ["a", "-tzip", '\"' + zipFile.fsName + '\"'];
  for (var i = 0; i < filelist.length; i++) {
    args.push('\"' + filelist[i].fsName + '\"');
  }
  this.execute(args);
};


This refactoring greatly simplies how to add well-structured access to an external program. But, in this case, it's not enough. Exec.execute does not block until the program finishes, which is needed with SevenZip.archive.

Code: Select all
Exec.prototype.toCommandStr = function(args) {
  var str;
  str = '\"' + this.app.fsName + '\"'; // the app

  if (args) {
    // if a file or folder was passed in, convert it to an array of one
    if (args instanceof File || args instanceof Folder) {
      args = ['\"' + args+ '\"'];
    }

    // if its an array, appened it to the command (could use Array.join)
    if (args.constructor == Array) {
      for (var i = 0; i < args.length; i++) {
        str += ' ' + args[i];
      }
    } else {
      // if its something else (like a prebuilt command string), just append it.
      str += ' ' + args.toString();
    }
  }
  return str;
};

Exec.prototype.execute = function(argList) {
  var str = this.toCommandStr(argList);
  var bat = new File(this.temp + '/' + this.app.name + ".bat");
  bat.open("w");
  bat.writeln(str);
  bat.writeln("del \"" + bat.fsName + "\" > NUL");
  bat.close();
  bat.execute();
};

Exec.prototype.executeBlock = function(argList, timeout) {
  var str = this.toCommandStr(argList);
  var semaphore = new File();

  var bat = new File(this.tmp + '/' + this.app.name + ".bat");
  var semaphore = new File(this.tmp + '/' + this.app.name + ".sem")

  bat.open("w");
  bat.writeln(str);
  bat.writeln("echo Done > \"" + semaphore.fsName + '\"');
  bat.writeln("del \"" + bat.fsName + "\" > NUL");
  bat.close();
  bat.execute();

  if (timeout) {
    var parts = 10;
    timeout = timeout / parts;
    while (!semaphore.exists && parts) {
      $.sleep(timeout);
      parts--;
    }

    if (!parts && !semaphore.exists) {
      throw "Timeout exceeded for program " + this.app.name;
    }
  }
 
  semaphore.remove();
};

Code: Select all
SevenZip.archive = function(zipFile, filelist) {
  var args = ["a", "-tzip", zipFile];
  for (var i = 0; i < filelist.length; i++) {
    args.push(filelist[i]);
  }
  this.executeBlock(args, 10000);
};


With all of this code integrated, we can do the following:
Code: Select all
  var dir = new Folder("/h/pics");
  SevenZip.archive(new File("/c/tmp/pics.zip"), dir.getFiles("*.jpg"));


After running this, we now have a new zip file with all of the jpgs from h:\pics


Collecting Output

With blocking in place, we can now move on to the next step: collecting output from a progam. The output we are collecting here is text written to stdout (the terminal) that can be redirected on the command line or text written to an output file that we can specify on the command line.

For this example we will again use Irfan. It has a "/info" option that will print out information about a file (or files) that looks like this:
Code: Select all
C:\tmp>"c:\Program Files\IrfanView3.91\i_view32.exe" h:\pics\veronica_01.jpg /info=c:\tmp\info.txt /killmesoftly /slient
C:\tmp>type info.txt
[veronica_01.jpg]
File name = veronica_01.jpg
Directory = H:\pics\
Compression = JPEG/JFIF
Resolution = 100 x 100 DPI
Image dimensions = 768 x 1024  Pixels
Print size = 19.5 x 26.0 cm; 7.7 x 10.2 inches
Color depth = 16,7 Millions   (24 BitsPerPixel)
Number of unique colors = 106869
Disk size = 144.76 KB (148,231 Bytes)
Current memory size = 2.25  MB (2,359,336 Bytes)
File date/time = 2/18/2005 / 19:01:04


In this case, we specified the output file on the command line then 'type'd it out to the terminal. This makes the implementation a little more complicated, but still very useful. We also specified the '/killmesoftly' which closes Irfan after the run and '/slient' which will skip over files that cause reading problems (like non-image files).

There are several different ways to implement this depending on the API. My preference for an API for this would be:
Code: Select all
 Irfan.info = function(arg)
// Where args is a file, a folder, or a file mask (using a WinXP path file mask)
// The method returns the output from Irfan
// Example: var str = Irfan.info(Folder("/h/pics"));


Here is one way to implement this:
Code: Select all
Irfan.info = function(arg) {
  if (!arg) {
    return '';
  }
  var args = [];

  if (arg instanceof File) {
    args.push('\"' + arg.fsName + '\"');
  } else if (arg instanceof Folder) {
    args.push('\"' + arg.fsName + "\\*\"");
  } else {
    args.push(arg.toString());
  }

  var ifile = new File(this.tmp + "/info.txt");

  args.push("/info=\"" + ifile.fsName + '\"');
  args.push("/killmesoftly");
  args.push("/silent");
  this.executeBlock(args, 10000);

  ifile.open("r");
  var str = ifile.read();
  ifile.close();
  ifile.remove();

  return str;
};


The calling script can then parse through the returned results and get all kinds of interesting information about an image without having to open the image up beforehand.


Summary

This should provide you with a good idea of what can be done with File.execute().

If you have problems, comment out the lines where the batch file is told to delete itself:
Code: Select all
  bat.writeln("del \"" + bat.fsName + "\" >NUL");

and examine the generated batch file. Edit that batch file until does what you want and then merge any needed changes back into your script.

Final Version
This final version has some enhancements (some necessary). The biggest it the addition of a timestamp in the batch file name. This is needed if you are in a tight loop and the previous call to a program is not yet complete. Another addition is the Exec.system function which simplifies executing a command and gathering and returning any output. See 'main' for an example usage.

Code: Select all
//
// Exec
//
Exec = function(app){
  this.app = app;         // A File object to the application
  this.tmp = Folder.temp; // You can use a different folder here
};

Exec.prototype.toCommandStr = function(args) {
  var str = '';

  if (this.app) {
    str = '\"' + this.app.fsName + '\"'; // the app
  }

  if (args) {
    // if a file or folder was passed in, convert it to an array of one
    if (args instanceof File || args instanceof Folder) {
      args = ['\"' + args+ '\"'];
    }

    // if its an array, appened it to the command (could use Array.join)
    if (args.constructor == Array) {
      for (var i = 0; i < args.length; i++) {
        str += ' ' + args[i];
      }
    } else {
      // if its something else (like a prebuilt command string), just append it.
      str += ' ' + args.toString();
    }
  }
  return str;
};

Exec.prototype.getBatName = function() {
  var nm = '';
  var ts = new Date().getTime();

  if (this.app) {
    nm = this.tmp + '/' + this.app.name + '-' + ts + ".bat";
  } else {
    nm = this.tmp + "/exec-" + ts + ".bat";
  }
  return nm;
};

Exec.system = function(cmd, timeout) {
  var ts = new Date().getTime();
  var e = new Exec();
  var outf = new File(e.tmp + "/exec-" + ts + ".out");
  e.executeBlock(cmd + "> \"" + outf.fsName + '\"', 5000);

  outf.open("r");
  var str = outf.read();
  outf.close();
  outf.remove();
  return str;
};

Exec.prototype.execute = function(argList) {
  var str = this.toCommandStr(argList);
  var bat = new File(this.getBatName());
  bat.open("w");
  bat.writeln(str);
  bat.writeln("del \"" + bat.fsName + "\" >NUL");
  bat.close();
  bat.execute();
};

Exec.prototype.block = function(semaphore, timeout) {
  if (timeout) {
    var parts = 10;            // wait for 1/10 of the timeout in a loop
    timeout = timeout / parts;

    while (!semaphore.exists && parts) {
      $.sleep(timeout);
      parts--;
    }

    if (!parts && !semaphore.exists) {
      throw "Timeout exceeded for program " + this.app.name;
    }
  }
};

Exec.prototype.executeBlock = function(argList, timeout) {
  var str = this.toCommandStr(argList);

  var bat = new File(this.getBatName());
  var semaphore = new File(bat.toString() + ".sem")

  bat.open("w");
  bat.writeln(str);
  bat.writeln("echo Done > \"" + semaphore.fsName + '\"');
  bat.writeln("del \"" + bat.fsName + "\" >NUL");
  bat.close();
  bat.execute();

  try {
    this.block(semaphore, timeout);
  } finally {
    semaphore.remove();
  }
};

//
// Irfan
//
Irfan = new Exec(new File("/c/Program Files/IrfanView3.91/i_view32.exe"));
Irfan.slideshow = function(folder) {
  var args = ["/slideshow=" + folder.fsName, "/closeslideshow"];
  this.execute(args);
};
Irfan.info = function(arg) {
  if (!arg) {
    return '';
  }
  var args = [];

  if (arg instanceof File) {
    args.push('\"' + arg.fsName + '\"');
  } else if (arg instanceof Folder) {
    args.push('\"' + arg.fsName + "\\*\"");
  } else {
    args.push(arg.toString());
  }

  var ifile = new File(this.tmp + "/info.txt");

  args.push("/info=\"" + ifile.fsName + '\"');
  args.push("/killmesoftly");
  args.push("/silent");
  this.executeBlock(args, 10000);

  ifile.open("r");
  var str = ifile.read();
  ifile.close();
  ifile.remove();

  return str;
};

//
// SevenZip
//
SevenZip = new Exec(new File("/c/Program Files/7-Zip/7z.exe"));
SevenZip.archive = function(zipFile, filelist) {
  var args = ["a", "-tzip", '\"' + zipFile.fsName + '\"'];
  for (var i = 0; i < filelist.length; i++) {
    args.push('\"' + filelist[i].fsName + '\"');
  }
  this.executeBlock(args, 10000);
};

// Test code

function main() {
  var dir = new Folder("/h/pics");

  Irfan.slideshow(dir);
  SevenZip.archive(new File("/c/tmp/pics.zip"), dir.getFiles("*.jpg"));

  alert(Irfan.info(dir));
  alert(Exec.system("dir " + dir.fsName, 10000));
};

main();

"Exec.jsx";
// EOF
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Andrew » Tue Dec 06, 2005 7:59 pm

These tutorials you are pushing out are GREAT X, really valuable.

As someone at the Adobe forum once said, what's the difference between an undocumented capability and no capability at all. Not much.

Andrew
Andrew
Site Admin
 
Posts: 775
Joined: Thu May 19, 2005 5:56 am
Location: New Zealand

Postby Andrew » Tue Jan 10, 2006 9:10 pm

I am currently messing around with writing from a JS to a MySQL database and reading from that db back to JS. I would also like to be able to apply this capability to w2k and more importantly OS X. Any further info on how to do the .bat equivalent on these platforms would be much appreciated.

Andrew
Andrew
Site Admin
 
Posts: 775
Joined: Thu May 19, 2005 5:56 am
Location: New Zealand

Postby xbytor » Tue Jan 10, 2006 11:08 pm

OK. I'll try to put together a bash version. I don't have a Mac to test on but I'll get it as close as I can.

ciao,
-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Rory » Thu Mar 30, 2006 6:25 pm

Thanks a million for this tutorial. Exactly what I needed!
Rory
Rory
 
Posts: 13
Joined: Tue Feb 28, 2006 5:25 am
Location: Nanaimo, BC Canada

Postby Mike Hale » Sat Sep 23, 2006 11:50 am

Hi X,

Is this version of Exec still your most current version?
Mike Hale
Site Admin
 
Posts: 4332
Joined: Fri Sep 30, 2005 10:52 pm
Location: USA

Postby xbytor » Sat Sep 23, 2006 2:56 pm

It's basically the same as my local version. The biggest change is splitting Exec.block into two function:
Code: Select all
Exec.blockForFile = function(file, timeout) {
  if (timeout) {
    var parts = 20;   // default to 1/20 of timeout intervals

    // if the timeout is more than 20 seconds, check every 2 seconds
    if (timeout > 20000) {
      parts = Math.ceil(timeout/2000);
    }

    timeout = timeout / parts;

    //$.writeln(parts + " parts, timeout: " + timeout);

    while (!file.exists && parts) {
      //$.write('.');
      $.sleep(timeout);
      parts--;
    }
  }
  return file.exists;
};

Exec.prototype.block = function(semaphore, timeout) {
  if (!Exec.blockForFile(semaphore, timeout)) {
    throw "Timeout exceeded for program " + this.app.name;
  }
};


I had a project where I had to specify an output file to the app instead of just collecting stdout (console output). I ran the app then blocked for that output file.

-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Mel » Mon Sep 25, 2006 8:42 pm

But How do you enable the sSEC_FILE_ALLOW_EXECUTE Key?

Phtoshop 7 requires it to run file.execute method.

I suspect its a registry key, but I'm not sure where to put it.
Mel
 
Posts: 18
Joined: Tue Sep 19, 2006 5:09 pm

Postby xbytor » Mon Sep 25, 2006 9:07 pm

As far as I know, there is not sSEC_FILE_ALLOW_EXECUTE key regardless of what the docs say. Try something like:

Code: Select all
new File("/c/Windows/explorer.exe").execute();


If that works, write a batch file and try executing that.

I honestly don't remember if File.execute() works on PS7 but this is how it would work.

-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Mel » Mon Sep 25, 2006 9:13 pm

xbytor wrote:As far as I know, there is not sSEC_FILE_ALLOW_EXECUTE key regardless of what the docs say. Try something like:

Code: Select all
new File("/c/Windows/explorer.exe").execute();


If that works, write a batch file and try executing that.

I honestly don't remember if File.execute() works on PS7 but this is how it would work.

-X


No still gives Cannot execute command error.
Mel
 
Posts: 18
Joined: Tue Sep 19, 2006 5:09 pm

Postby xbytor » Mon Sep 25, 2006 9:21 pm

I'll try and remember to fire up PS7 this evening and find out what's going on. It may just not be possible.

-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Mike Hale » Tue Sep 26, 2006 2:00 am

X,

I have two questions about a post you made at the Adobe site extending Exec to exiftool.

1. Is that something you are working on or just something you put together to show him how to use Exec?

2. I my not be using the right terms but in that post, you made Exiftool a class(subclass?) and in this tutorial you made Irfan an Exec object. Is there an advantage in doing one way or the other?

Mike
Mike Hale
Site Admin
 
Posts: 4332
Joined: Fri Sep 30, 2005 10:52 pm
Location: USA

Postby xbytor » Tue Sep 26, 2006 4:09 pm

Mike Hale wrote:I have two questions about a post you made at the Adobe site extending Exec to exiftool.

1. Is that something you are working on or just something you put together to show him how to use Exec?


I actually sketched that out awhile ago. I never made any progress beyond what I posted. I started focusing my energy on my XMPTool set of classes.

2. I my not be using the right terms but in that post, you made Exiftool a class(subclass?) and in this tutorial you made Irfan an Exec object. Is there an advantage in doing one way or the other?


In this case, not really. Using objects hopefully made the tutorial a bit clearer. I typcially use classes when I have multiple properties to deal with that I need to keep track of from one method call to another. In the Irfan case, there were no properties to deal with, just new methods. I wasn't sure what direction the exiftool case was going to take so I started off subclassing. If it turns out that it didn't need state to persist from one method invocation to another, I may have converted it back to an object implementation.

-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Postby Mike Hale » Fri Jan 12, 2007 1:55 am

Hi X,

I just used your exec.js from your current Xtools. Did you split executeBlock into another script?

Mike
Mike Hale
Site Admin
 
Posts: 4332
Joined: Fri Sep 30, 2005 10:52 pm
Location: USA

Postby xbytor » Fri Jan 12, 2007 2:00 am

The current rev of this is called xexec.js. You may want to switch over.

It has the benefit of also working on the Mac, with a little setup. I use it in CSX to call exiftool on both the Mac and XP. exiftool is real handy for pulling preview/thumbnail images out of files which greatly speeds up contact sheet creation. It's not something I advertise much because getting it correctly configured can sometimes be a chore.

I've also implemented better logging. It's essential to get this stuff working correctly. Time permitting, I'll write something up on the rev including the Mac setup script.

-X
xbytor
Site Admin
 
Posts: 2275
Joined: Thu May 19, 2005 12:11 pm
Location: In Limbo

Next

Return to Photoshop Scripting: Reference, Documentation, & Tutorials

Who is online

Users browsing this forum: No registered users and 0 guests

cron