Mistral extensible design


Mistral is still very young (spinned off another project last summer) and needs work for completing the API, testing and documenting it. Nevertheless, in the last month it get involved in two small paid projects of mine:

  1. as the back end for a webapp used to manage a library of photos
  2. in form of a small applet to perform a few operations on faxes
(BTW, both the projects will be opensourced and I will talk more about them later). As it happens in such circumstances, I've been working on Mistral with specific focus on the issues related to the two projects (of course there's also the other main workflow, which is supporting the Pleiades project in distributed environments, where I'm working with Emmanuele).

Yesterday I got the first two email messages from people interested in it. The former was from a guy that told me "Mistral has failed the five minutes test". That is, he failed in downloading, compiling and trying some demo in a very short time. He was right, unfortunately. But it was simple to fix it, and most of the needed changes are already in the repository. The latter told me that he is evaluating Mistral, he's using it, but misses some simple features to go on: basically, he needs to strip the "transparency" channel from some PNG images before resizing and writing them in JPEG format (that doesn't support transparency). Fortunately, the flexible and simple design of Mistral allows to easily solve this kind of problems without the need of patches.

We've not been able yet to write a decent documentation about the design, so I will briefly talk about the main topics that are relevant for this blog entry (I'd like to add some diagrams, but unfortunately I'm in a peak period and I don't have time at the moment). These are the basic patterns in the design:

  1. Facade pattern. EditableImage is a facade, encapsulating the image model and other features (for instance, some boilerplate code to make it easier and more efficient the distribution of images in a parallel context). From the programmer perspective, EditableImage is everything he needs to manipulate an image.
  2. Command pattern. Operation is a generic class at the root of a hierarchy containing all the possible operations that you can apply to an image. For instance, PaintOp and CropOp both extend Operation and can be invoked as: editableImage.execute(new PaintOp(...)) and editableImage.execute(new CropOp(...)). Operations don't contain an implementation, but are just holders for the parameters needed to describe the specific the image manipulation to perform.
  3. Abstract Factory pattern. Behind the scenes, there are one or more factories (called "implementation registers") that are able to provide the real implementations related to each operation. There are many factories since multiple image models are supported (e.g. BufferedImage from Java2D, PlanarImage from JAI, ImagePlus from ImageJ). For instance, PaintOp is matched to a PaintJ2DOp and CropOp to a CropJ2DOp, and in the end the *J2DOp are invoked with the BufferedImage wrapped by the EditableImage.
The implementation registers are accessible to the programmer, that can easily create new operations and register them into the core. This can be accomplished in three steps:
  1. write the abstract Operation
  2. write the concrete implementation
  3. bind them in the ImplementationRegistry
For instance, to strip the transparency channel we can create a custom operation that changes the buffer type of the image to one of the many formats defined by the BufferedImage.TYPE_XXX constants - TYPE_3BYTE_BGR is what we need. The abstract Operation can be written as:
public class ChangeBufferTypeOp extends Operation
{
private final int bufferType;

public ChangeBufferTypeOp (final int bufferType)
{
if ((bufferType <= 0) || (bufferType > 13))
{
throw new IllegalArgumentException("bufferType: " + bufferType);
}

this.bufferType = bufferType;
}

public int getBufferType()
{
return bufferType;
}
}
The concrete implementation for Java2D is also simple:
public class ChangeBufferTypeJ2DOp extends 
OperationImplementation<ChangeBufferTypeOp, BufferedImage>
{
protected BufferedImage execute (final ChangeBufferTypeOp operation,
final BufferedImage bufferedImage)
{
final int width = bufferedImage.getWidth();
final int height = bufferedImage.getHeight();
final BufferedImage result =
new BufferedImage(width, height, operation.getBufferType());
Graphics g = null;

try
{
g = result.createGraphics();
g.drawImage(bufferedImage, 0, 0, null);
}
finally
{
if (g != null)
{
g.dispose();
}
}

return result;
}
}
The new operation and its implementation can be registered with the following code:
ImplementationFactoryJ2D.getInstance().
registerImplementation(ChangeBufferTypeOp.class, ChangeBufferTypeJ2DOp.class);
At this point, the new operation is available to the application just as the standard ones. For instance, the following code works:
File file = new File("photo.jpg");
EditableImage image = EditableImage.create(new ReadOp(file));
image.execute(new ChangeBufferTypeOp(BufferedImage.TYPE_3BYTE_BGR));
image.execute(new WriteOp("JPEG", new File("Result.tif")));
Mistral supports more sophisticated stuff. For instance, implementation registers are not mutually exclusive, they can be used at the same time if, for instance, one supports most of the operation you need, but not all (this frequently happens with Java2D and JAI). And if you want you can add an implementation register to deal with a completely new image model - some time ago a researcher sent me some prototype code about a new filter he is designing - he used his own image format, but we are integrating it inside Mistral and nevertheless application code will be able to use the standard Image I/O for the input/output. Another crazy idea that we're just studying is the possibility to implement a special implementation registry which binds to the CoreImage APIs of Mac OS X. We will publish something about these features later, since their design still needs a final refinement.

So, if you are curious about Mistral but you fear it's still too young and incomplete, just give it a try. Extending it is easy and you probably don't need to wait for us to improve the coverage of operations. And if you do and write some new operations for your needs, please consider sending them to us, provided by their JUnit testing code - we will be happy to add them to the source repository. All the contributions will be added in a special "Contributions" module, immediately available to everybody, and later incorporated in the official, supported APIs after some refinement, if needed. See you on our mailing list.