Writing POV-Ray Support for NetBeans VII - Support For Running POV-Ray from NetBeans
Tim Boudreau
7 June 2006
Feedback
This is a DRAFT!
This is a continuation of the tutorial for building POV-Ray support for
NetBeans. If you have not read the first
or second, third,
fourth fifth and
sixth
parts of this tutorial, you may want to start there.
Obtaining POV-Ray
At this point, we're almost ready to run code, so it would be a good idea
to download a copy of POV-Ray. Official builds can be obtained from
povray.org, but there is a caveat: The
Windows version of POV-Ray is a GUI executable, which will want to open
an editor rather than render your file to disk. So Windows users should
download an "unofficial" build such as
this one, or create
a command-line build using cygwin and gcc. Macintosh users may find
DarwinPorts the easiest
way - simply install DarwinPorts and then run
sudo port install povray. Linux and other Unix users should be
fine with the downloades available from povray.org.
Executing POV-Ray and Displaying the Output
At this point, we are ready to write the code that will actually invoke the
external POV-Ray executable, pass it the correct arguments and display its
output. POV-Ray has two kinds of output: It will write out status and
success failure on the command line, in a similar way to what a compiler
does, and it will write out an image file on disk, which we will want to
display.
The first part is just being able to invoke the external executable.
NetBeans has some API classes that can help with that.
- Add the following constructor and fields to the
Povray
class:
private final RendererServiceImpl renderService;
private final FileObject toRender;
private final Properties settings;
Povray (RendererServiceImpl renderService, FileObject toRender, Properties settings) {
this.renderService = renderService;
this.toRender = toRender;
this.settings = settings == null ? renderService.getRendererSettings(
renderService.getPreferredRendererSettingsName()) : settings;
}
- Next we will implement a method that will find the file to render.
We were passed a
FileObject, but now we need an actual
java.io.File to get the path from, to pass on the command
line to POV-Ray. There are two caveats: 1. The file passed to the
constructor may be null - in that case we should find the main file of
the project and use that; and 2. It is conceivable that the file will
not exist on disk - NetBeans filesystems are virtual, after all, and
the file could exist in a remote FTP filesystem or such. Since NetBeans
4.0, this is rather unlikely, but we should still test for this condition
(FileUtil.toFile() returns null). So we will add a method
to Povray as follows:
private File getFileToRender() throws IOException {
FileObject render = toRender;
if (render == null) {
PovrayProject proj = renderService.getProject();
MainFileProvider provider = (MainFileProvider)
proj.getLookup().lookup (MainFileProvider.class);
if (provider == null) {
throw new IllegalStateException ("Main file provider missing");
}
render = provider.getMainFile();
if (render == null) {
ProjectInformation info = (ProjectInformation)
proj.getLookup().lookup(ProjectInformation.class);
//XXX let the user choose
throw new IOException (NbBundle.getMessage(Povray.class,
"MSG_NoMainFile", info.getDisplayName()));
}
}
assert render != null;
File result = FileUtil.toFile (render);
if (result == null) {
throw new IOException (NbBundle.getMessage(Povray.class,
"MSG_VirtualFile", render.getName()));
}
assert result.exists();
assert result.isFile();
return result;
}
- Next we need to assemble the command-line arguments that need to
be passed to POV-Ray. These take the form of
+[some character][somevalue], for example,
+A0.9 sets the anti-aliasing parameter to 0.9 pixels.
So we need to iterate the Properties object passed to the
constructor and assemble from it a set of command line arguments:
private String getCmdLineArgs(File includesDir) {
StringBuffer cmdline = new StringBuffer();
for (Iterator i=settings.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
String val = settings.getProperty(key);
cmdline.append ('+');
cmdline.append (key);
cmdline.append (val);
cmdline.append (' ');
}
cmdline.append ("+L");
cmdline.append (includesDir.getPath());
return cmdline.toString();
}
- Next we need to implement a couple of utility methods that the
rendering method will use:
private File getImagesDir() {
PovrayProject proj = renderService.getProject();
FileObject fob = proj.getImagesFolder(true);
File result = FileUtil.toFile(fob);
assert result != null && result.exists();
return result;
}
private String stripExtension(File f) {
String sceneName = f.getName();
int endIndex;
if ((endIndex = sceneName.lastIndexOf('.')) != -1) {
sceneName = sceneName.substring(0, endIndex);
}
return sceneName;
}
Neither is terribly exciting - one gets the images directory from the
project as a java.io.File, and the other trims the file
extension off a file name (so we can create an image file with the same
name as the scene file).
- The next method we will add is another utility method. When we
render, we will want to show messages on the status bar that describe
what is happening - or what went wrong in the event of failure. The
UI Utilities API contains a class called
StatusDisplayer
that lets any code in NetBeans that wants to write to the status bar
(the actual implementation of StatusDisplayer is in the
windowing system implementations, core/windows in NetBeans
CVS).
Implement the following method, and then add a dependency on the
UI Utilities API module from the Povray Projects module:
private void showMsg (String msg) {
StatusDisplayer.getDefault().setStatusText(msg);
}
- At this point, we've added a bunch of status messages our code can
display, so it is time to add actual text for those messages to the
resource bundle. Note that in a number of cases we call:
NbBundle.getMessage(SomeClass.class, "MSG_Something", someStringArgument);
to fetch a localized string. NbBundle supports embedding
arguments inside of a localized string - you can either use the above
method, or a variant that takes an array of arguments to embed. So you
can define strings in a resource bundle using the syntax
Could not delete {0} because {1}
and {0} and {1} will be replaced by arguments
passed to getMessage(). This is extremely useful, as often
the order in which such strings occur in the result text will be different
in different human languages.
So let's go ahead and add the warning messages we need to
Bundle.properties in the same package as PovrayProject:
MSG_NoMainFile=Main scene file not set for {0}
MSG_VirtualFile=Not a file on disk: {0}
MSG_Rendering=Rendering {0}
MSG_NoPovrayExe=No POV-Ray executable, cannot render
MSG_NoPovrayInc=No POV-Ray includes dir, cannot render
MSG_Success=Rendered {0} successfully
MSG_Failure=Failed to render {0}
MSG_CantDelete=Could not delete {0}, it is locked or in use
- Now we are almost ready to get down to the nitty-gritty of actually
invoking POV-Ray from NetBeans. We will do this in the standard Java
way, using
Runtime.exec() to start an external process.
We also will want to display the text output from the process as it
reports its progress, in the output window. This means we will need a
way to write to the output window. So we will add one more dependency
to Povray Projects - add a dependency on the IO API module (use the class
name InputOutput in the Add Dependency dialog).
- Handling output from a process is tricky - we will actually have
three threads running to handle our process:
- The thread that invoked the process and is waiting for it to
terminate
- A thread that is collecting output from the standard output of
the POV-Ray process and writing it to the output window
- Another thread that is doing the same thing for the error output
of the POV-Ray process
So we will need some kind of Runnable which will wait for
data from each output stream and route it to the output window in
NetBeans as it becomes available. Writing to the output window is quite
easy - you get an InputOutput object from
IOProvider.getDefault() and then write to one of its streams -
for example
InputOutput io = IOProvider.getDefault().getIO ("Hello", true);
io.select();
io.getOut().println ("Hello world");
io.getErr().println ("This is the standard error output - it should be red");
is all it takes to make the output window pop up and display some output.
So before we implement the code that will create the process, lets create
the runnable that will wait for output from the process and route it to
the output window - it will be a static nested class inside the
Povray class:
static class OutHandler implements Runnable {
private Reader out;
private OutputWriter writer;
public OutHandler (Reader out, OutputWriter writer) {
this.out = out;
this.writer = writer;
}
public void run() {
while (true) {
try {
while (!out.ready()) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
close();
return;
}
}
if (!readOneBuffer() || Thread.currentThread().isInterrupted()) {
close();
return;
}
} catch (IOException ioe) {
//Stream already closed, this is fine
return;
}
}
}
private boolean readOneBuffer() throws IOException {
char[] cbuf = new char[255];
int read;
while ((read = out.read(cbuf)) != -1) {
writer.write(cbuf, 0, read);
}
return read != -1;
}
private void close() {
try {
out.close();
} catch (IOException ioe) {
ErrorManager.getDefault().notify(ioe);
} finally {
writer.close();
}
}
}
- Now we are ready to implement the
render() method that
will invoke POV-Ray. This method should be never be invoked from
the event thread, because it would block the UI until POV-Ray is finished.
So the first thing we do is sanity check what thread we're running on.
Then we get the file we need to render, sanity checking that. Then we
call getPovray() which may open a file chooser to let the
user pick it, and similarly get the default include directory which we
will need to pass on the command line. Then we get the directory where
we will put the output, assemble our output file name (we use PNG format
since NetBeans' Image module supports that). Then we compute the command
line that should be passed to POV-Ray. Then we call
Runtime.exec() with that argument, wire up the output window
to the output streams from the resulting process, and wait for the
process to exit. Once it exits, we determine if it succeeded or failed,
show an appropriate message, and if it succeeded, return a
FileObject representing the file that was created.
public FileObject render () throws IOException {
if (EventQueue.isDispatchThread()) {
throw new IllegalStateException ("Tried to run povray from the " +
"event thread");
}
//Find the scene file pass to POV-Ray as a java.io.File
File scene;
try {
scene = getFileToRender();
} catch (IOException ioe) {
showMsg (ioe.getMessage());
return null;
}
//Get the POV-Ray executable
File povray = getPovray();
if (povray == null) {
//The user cancelled the file chooser w/o selecting
showMsg(NbBundle.getMessage(Povray.class, "MSG_NoPovrayExe"));
return null;
}
//Get the include dir, if it isn't under povray's home dir
File includesDir = getStandardIncludeDir(povray);
if (includesDir == null) {
//The user cancelled the file chooser w/o selecting
showMsg (NbBundle.getMessage(Povray.class, "MSG_NoPovrayInc"));
return null;
}
//Find the image output directory for the project
File imagesDir = getImagesDir();
//Assemble and format the line switches for the POV-Ray process based
//on the contents of the Properties object
String args = getCmdLineArgs(includesDir);
String outFileName = stripExtension (scene) + ".png";
//Compute the name of the output image file
File outFile = new File(imagesDir.getPath() + File.separator +
outFileName);
//Delete the image if it exists, so that any current tab viewing the file is
//closed and the file will definitely be re-read when it is re-opened
if (outFile.exists() && !outFile.delete()) {
showMsg (NbBundle.getMessage(Povray.class,
"LBL_CantDelete", outFile.getName()));
return null;
}
//Append the input file and output file arguments to the command line
String cmdline = povray.getPath() + ' ' + args + " +I" +
scene.getPath() + " +O" + outFile.getPath();
System.err.println(cmdline);
showMsg (NbBundle.getMessage(Povray.class, "MSG_Rendering",
scene.getName()));
final Process process = Runtime.getRuntime().exec (cmdline);
//Get the standard out of the process
InputStream out = new BufferedInputStream (process.getInputStream(), 8192);
//Get the standard in of the process
InputStream err = new BufferedInputStream (process.getErrorStream(), 8192);
//Create readers for each
final Reader outReader = new BufferedReader (new InputStreamReader (out));
final Reader errReader = new BufferedReader (new InputStreamReader (err));
//Get an InputOutput to write to the output window
InputOutput io = IOProvider.getDefault().getIO(scene.getName(), false);
//Force it to open the output window/activate our tab
io.select();
//Print the command line we're calling for debug purposes
io.getOut().println(cmdline);
//Create runnables to poll each output stream
OutHandler processSystemOut = new OutHandler (outReader, io.getOut());
OutHandler processSystemErr = new OutHandler (errReader, io.getErr());
//Get two different threads listening on the output & err
//using the system-wide thread pool
RequestProcessor.getDefault().post(processSystemOut);
RequestProcessor.getDefault().post(processSystemErr);
try {
//Hang this thread until the process exits
process.waitFor();
} catch (InterruptedException ex) {
ErrorManager.getDefault().notify(ex);
}
//Close the output window's streams (title will become non-bold)
processSystemOut.close();
processSystemErr.close();
if (outFile.exists() && process.exitValue() == 0) {
//Try to find the new image file
FileObject outFileObject = FileUtil.toFileObject(outFile);
showMsg (NbBundle.getMessage(Povray.class, "MSG_Success",
outFile.getPath()));
return outFileObject;
} else {
showMsg (NbBundle.getMessage(Povray.class, "MSG_Failure",
scene.getPath()));
return null;
}
}
- The last thing is to fix our implementation of
RendererService to call Povray.render().
Open RendererServiceImpl in the code editor, and modify
the render method:
public FileObject render(FileObject scene, Properties renderSettings) {
Povray pov = new Povray (this, scene, renderSettings);
try {
return pov.render();
} catch (IOException ioe) {
ErrorManager.getDefault().notify(ErrorManager.USER, ioe);
return null;
}
}
- The last step is to open the image when the rendering process is
complete. This is quite simple to implement - we just need to look
for an
OpenCookie on the Node for the image
that was rendered. If you are running a standard configuration of the
NetBeans IDE, you already have the Image module installed - it will
provide support for opening an image, displaying it in the editor area.
So implement RendererAction.RenderWithSettingsAction.run() like
this:
public void run() {
DataObject ob = node.getDataObject();
FileObject toRender = ob.getPrimaryFile();
Properties mySettings = renderer.getRendererSettings(name);
FileObject image = renderer.render(toRender, mySettings);
if (image != null) {
try {
DataObject dob = DataObject.find (image);
Node n = dob.getNodeDelegate();
OpenCookie ck = (OpenCookie) n.getLookup().lookup(
OpenCookie.class);
if (ck != null) {
ck.open();
}
} catch (DataObjectNotFoundException e) {
//Should never happen
ErrorManager.getDefault().notify(e);
}
}
}
With that, you should be able to clean, build and run the module suite, and
be able to run POV-Ray and generate an image in the images/
subdirectory of your project.
Next Steps
The next section will cover implementing
ViewService and adding actions for that.
|
|