Writing POV-Ray Support for NetBeans VII—Support For Running POV-Ray
This tutorial needs a review. You can edit it in GitHub following these contribution guidelines. |
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.
-
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 downloads available from povray.org, using these instructions. Everything should work out of the box for these users, without any tweaks or post-install configurations. Make sure, however, that the POV-Ray launcher has the correct permissions, otherwise it will not execute. Run it from the command line to check that it executes.
For details on setting up POV-Ray, especially for detailed instructions for Windows, see the Appendix. |
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.
At the end of this part of the tutorial, you will have an application that looks like this (click to enlarge the image):
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 actualjava.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 toPovray
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 theProperties
object passed to the constructor and assemble from it a set of command line arguments:
private String getCmdLineArgs(File includesDir) {
StringBuilder cmdline = new StringBuilder();
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 ofStatusDisplayer
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", _[.underline]#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 nameInputOutput
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;
}
@Override
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) {
Exceptions.printStackTrace(ioe);
} finally {
writer.close();
}
}
}
-
Now we are ready to implement the
render()
method in thePovray
class, in the Povray Project module, that will invokePOV-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 callgetPovray()
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) {
Exceptions.printStackTrace(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 callPovray.render()
. OpenRendererServiceImpl
in the code editor, and modify the render method:
@Override
public FileObject render(FileObject scene, Properties renderSettings) {
Povray pov = new Povray (this, scene, renderSettings);
try {
return pov.render();
} catch (IOException ioe) {
Exceptions.printStackTrace(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 theNode
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 implementRendererAction.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
Exceptions.printStackTrace(e);
}
}
}
With that, you should be able to clean, build and run the application 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.