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
