package com.srbenoit.filter.items;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import com.srbenoit.filter.AbstractPipeItem;
import com.srbenoit.filter.FilterTreeExecutor;
import com.srbenoit.filter.Pipe;
import com.srbenoit.filter.PipeItemFileInfo;
import com.srbenoit.xml.ElementBase;
import com.srbenoit.xml.EmptyElement;
import com.srbenoit.xml.Node;
import com.srbenoit.xml.XmlParser;
/**
* An array of images that are backed by files. This class includes methods to create such an image
* and retrieve the BufferedImage
, to load it from its backing file (if present), and
* write it to its backing file.
*
*
We maintain an array of file information objects in parallel with the image objects. While
* the image may or may not be loaded at any given time, the presence of a file information object
* with persisted = true indicates that there is a file with the image data and we can load the
* file.
*/
public class ImageArrayPipeItem extends AbstractPipeItem {
/** format string for JPEG files */
public static final String FORMAT_JPEG = "JPEG";
/** format string for PNG files */
public static final String FORMAT_PNG = "PNG";
/** format string for TIFF files */
public static final String FORMAT_TIFF = "TIFF";
/** the image (either created by this class, or loaded from the file) */
private transient BufferedImage[][] images;
/** the file info for each image file */
private transient PipeItemFileInfo[][] fileInfo;
/** the label for the X axis (typically "x" or "t") */
private transient String xLabel;
/** the label for the Y axis (typically "y" or "z") */
private transient String yLabel;
/** the file suffix for the image file format */
private transient String type;
/**
* Constructs a new empty ImageArrayPipeItem
.
*
* @param theKey the unique key for the item
* @param theLabel the label for the item (a human friendly name)
* @param thePipe the pipe in which this item is installed
*/
public ImageArrayPipeItem(final String theKey, final String theLabel, final Pipe thePipe) {
super(theKey, theLabel, thePipe);
File file;
this.images = new BufferedImage[0][0];
this.fileInfo = new PipeItemFileInfo[0][0];
this.xLabel = "";
this.yLabel = "";
this.type = "";
ImageIO.scanForPlugins();
file = makeFile(getSubdir());
addFile(new PipeItemFileInfo(file));
}
/**
* Constructs a new ImageArrayPipeItem
with a newly allocated
* BufferedImage
of a given size and type. The new (blank) image is not written to the
* file.
*
* @param theKey the unique key for the item
* @param theLabel the label for the item (a human friendly name)
* @param thePipe the pipe in which this item is installed
* @param theXLabel the label for the X axis (typically "x" or "t")
* @param theYLabel the label for the Y axis (typically "y" or "z")
* @param numX the number of images in the X (or time) direction
* @param numY the number if images in the Y (or plane) direction
* @param fileSuffix the file suffix for images written to disk (determines format)
*/
public ImageArrayPipeItem(final String theKey, final String theLabel, final Pipe thePipe,
final String theXLabel, final String theYLabel, final int numX, final int numY,
final String fileSuffix) {
super(theKey, theLabel, thePipe);
File file;
if (numX < 1) {
throw new IllegalArgumentException("Number of time points be at least 1");
}
if (numY < 1) {
throw new IllegalArgumentException("Number of planes be at least 1");
}
if (fileSuffix == null) {
throw new IllegalArgumentException("File suffix may not be null");
}
this.images = new BufferedImage[numX][numY];
this.fileInfo = new PipeItemFileInfo[numX][numY];
this.xLabel = theXLabel;
this.yLabel = theYLabel;
setType(fileSuffix);
file = makeFile(getSubdir());
addFile(new PipeItemFileInfo(file));
}
/**
* Sets the file type to which images should be saved.
*
* @param type the file type (extension)
* @throws IllegalArgumentException if the type is not valid
*/
private void setType(final String type) throws IllegalArgumentException {
boolean found;
String[] suffixes;
StringBuilder str;
ImageIO.scanForPlugins();
found = false;
suffixes = ImageIO.getReaderFileSuffixes();
for (String test : suffixes) {
if (test.equals(type)) {
found = true;
break;
}
}
if (!found) {
str = new StringBuilder(80);
str.append("Invalid image file suffix '");
str.append(type);
str.append("' (supported suffixes are");
for (String test : suffixes) {
str.append(' ');
str.append(test);
}
throw new IllegalArgumentException(str.toString());
}
this.type = type;
}
/**
* Sets the image at a particular X and Y position.
*
* @param xPos the X position
* @param yPos the Y position
* @param img the image
*/
public void setImage(final int xPos, final int yPos, final BufferedImage img) {
File sub;
File file;
this.images[xPos][yPos] = img;
if (this.fileInfo[xPos][yPos] != null) {
removeFile(this.fileInfo[xPos][yPos]);
}
sub = new File(getPipe().getDir(), getKey());
file = makeImageFile(sub, xPos, yPos);
this.fileInfo[xPos][yPos] = new PipeItemFileInfo(file);
addFile(this.fileInfo[xPos][yPos]);
}
/**
* Sets the file suffix for files written by this pipe.
*
* @param fileSuffix the file suffix for images written to disk (determines format)
*/
public void setFileSuffix(final String fileSuffix) {
File sub;
if (fileSuffix == null) {
throw new IllegalArgumentException("File suffix may not be null");
}
if (!this.type.equals(fileSuffix)) {
this.type = fileSuffix;
getFile(0).notPersisted();
// We need to rebuild all file info objects and mark everything as not persisted
sub = getSubdir();
for (int x = 0; x < this.fileInfo.length; x++) {
for (int y = 0; y < this.fileInfo[x].length; y++) {
if (this.fileInfo[x][y] != null) {
this.fileInfo[x][y].setFile(makeImageFile(sub, x, y));
this.fileInfo[x][y].notPersisted();
}
}
}
}
}
/**
* Gets the size of the image array along the X direction.
*
* @return the array X dimension
*/
public int getXSize() {
return this.images.length;
}
/**
* Gets the size of the image array along the Y direction.
*
* @return the array Y dimension
*/
public int getYSize() {
return this.images[0].length;
}
/**
* Gets the label for the X dimension.
*
* @return the label
*/
public String getXLabel() {
return this.xLabel;
}
/**
* Gets the label for the Y dimension.
*
* @return the label
*/
public String getYLabel() {
return this.yLabel;
}
/**
* Gets the image at a particular X and Y position. If the image is cached but has not been
* loaded, this loads the image then returns it.
*
* @param xPos the X position
* @param yPos the Y position
* @return the image, or null
if no image has been set
*/
public BufferedImage getImage(final int xPos, final int yPos) {
File sub;
File file;
sub = getSubdir();
if (this.images[xPos][yPos] == null) {
file = makeImageFile(sub, xPos, yPos);
if (file.exists()) {
this.images[xPos][yPos] = loadImage(file);
}
}
return this.images[xPos][yPos];
}
/**
* Gets the set of images at a particular X position. If any image in the set is cached but has
* not been loaded, this loads the image before returning the array.
*
* @param xPos the X position
* @return the images, any of which may be null
*/
public BufferedImage[] getImages(final int xPos) {
File sub;
File file;
sub = getSubdir();
for (int y = 0; y < this.images[xPos].length; y++) {
if (this.images[xPos][y] == null) {
file = makeImageFile(sub, xPos, y);
if (file.exists()) {
this.images[xPos][y] = loadImage(file);
}
}
}
return this.images[xPos];
}
/**
* Loads an image from the filesystem.
*
* @param file the file to load
* @return the loaded image
*/
private BufferedImage loadImage(final File file) {
BufferedImage img;
try {
img = ImageIO.read(file);
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to load image " + file.getAbsolutePath(), e);
img = null;
}
return img;
}
/**
* Gets a human-friendly name for the data type. For example, a list of sets of images
* representing a time series of z-planes might return "Multi-plane image sequence".
*
* @return the name of the data type this item represents
*/
@Override public String typeName() {
return "Image Array";
}
/**
* Resets the pipe item to a virgin (empty) state.
*/
@Override public void reset() {
for (int x = 0; x < this.images.length; x++) {
for (int y = 0; y < this.images[x].length; y++) {
this.images[x][y] = null;
this.fileInfo[x][y] = null;
}
}
removeAllFilesButFirst();
getFile(0).notPersisted();
}
/**
* Saves the item to a filesystem.
*
* @param executor the executor that is saving the pipe
* @param startPct the starting progress percentage for the save operation
* @param endPct the ending progress percentage for the save operation
* @return true
if the save succeeded; false
if not
*/
@Override public boolean save(final FilterTreeExecutor executor, final int startPct,
final int endPct) {
int total;
int soFar;
File sub;
StringBuilder str;
boolean result;
PipeItemFileInfo info;
sub = getSubdir();
if (!sub.exists()) {
sub.mkdir();
}
// Count the number of files we need to write
total = 0;
for (int x = 0; x < this.images.length; x++) {
for (int y = 0; y < this.images[x].length; y++) {
info = this.fileInfo[x][y];
if ((info != null) && (!info.isPersisted())) {
total++;
}
}
}
result = true;
soFar = 0;
for (int x = 0; x < this.images.length; x++) {
for (int y = 0; y < this.images[x].length; y++) {
info = this.fileInfo[x][y];
if ((info != null) && (!info.isPersisted())) {
soFar++;
executor.indicateProgress(startPct + (soFar * (endPct - startPct) / total));
result = persistImage(sub, x, y);
if (result) {
info.wasPersisted();
} else {
break;
}
}
}
}
if (result) {
executor.indicateProgress(endPct);
info = getFile(0);
str = new StringBuilder(500);
str.append("true
if all non-null images were written to the disk successfully;
* false
if not
*/
private boolean persistImage(final File sub, final int xPos, final int yPos) {
File file;
boolean result;
file = makeImageFile(sub, xPos, yPos);
try {
ImageIO.write(this.images[xPos][yPos], this.type, file);
result = true;
} catch (IOException e) {
LOG.log(Level.WARNING, "Exception writing image file", e);
result = false;
}
return result;
}
/**
* Loads the items from the filesystem
*
* @return true
if the load succeeded; false
if not
*/
@Override public boolean load() {
File sub;
File file;
byte[] bytes;
List