# Extending ImageJ: Data I/O¶

This notebook demonstrates how to write an IOPlugin, which handles reading and/or writing of external data to and from Java data structures.

In [1]:
%classpath config resolver scijava.public https://maven.scijava.org/content/groups/public
%classpath add mvn net.imagej imagej 2.0.0-rc-71
ij = new net.imagej.ImageJ()
"ImageJ v${ij.getVersion()} is ready to go."  Added new repo: scijava.public  Out[1]: ImageJ v2.0.0-rc-71 is ready to go. ## Creating a data reader¶ Suppose we have data stored in a file format, .char-table, which represents a table as a sequence of characters, along with metadata defining the number of rows and columns. We would like to write a plugin so that ImageJ can import these files via its File ▶ Open command. The format of a .char-table file is key-value pairs, one per line. Valid keys are: • cols to define the column count • rows to define the row count • data to specify the actual character data: a sequence of$cols \times rows$characters enumerated in row-major order, and bracketed by < and >. Let's start by writing out an example .char-table file, which we will use for testing: In [2]: import java.io.File import org.scijava.util.DigestUtils import org.scijava.util.FileUtils data = """ cols = 7 rows = 7 data = <@@@@@@@@ @ @ @ @@ @ @ @@ @ @ @@ @ @ @@@ > """ desktop = System.getProperty("user.home") + "/Desktop/" tablePath = desktop + "example.char-table" outFile = new File(tablePath) FileUtils.writeFile(outFile, DigestUtils.bytes(data)) "Wrote${outFile.length()} bytes to '$outFile'"  Out[2]: Wrote 78 bytes to '/Users/curtis/Desktop/example.char-table' Without further ado, here is the IOPlugin implementation: In [3]: import java.io.File import org.scijava.io.AbstractIOPlugin import org.scijava.io.IOPlugin import org.scijava.plugin.Plugin import org.scijava.table.DefaultGenericTable import org.scijava.table.Table import org.scijava.util.DigestUtils import org.scijava.util.FileUtils @Plugin(type = IOPlugin.class) public class CharTableReader extends AbstractIOPlugin<Table> { @Override public Class<Table> getDataType() { // This is the type of object produced by the reader. return Table.class } @Override public boolean supportsOpen(final String source) { // Check whether the source is indeed a .char-table. // This check can be as shallow or as deep as you want, // but it is advised to keep it as fast as possible. // As such, it is not recommended to actually open and // interrogate the source unless you have no choice. return source.toLowerCase().endsWith(".char-table") } @Override public Table open(final String source) throws IOException { // This is where we read the data from its source, // and convert it into the destination data type. // Read in the file. String contents = DigestUtils.string(FileUtils.readFile(new File(source))) // Parse the contents line by line. int rows = 0, cols = 0 String data = null for (line in contents.split("\n")) { int equals = line.indexOf("=") if (equals < 0) continue String key = line.substring(0, equals).trim() String val = line.substring(equals + 1).trim() switch (key) { case "rows": rows = Integer.parseInt(val) break case "cols": cols = Integer.parseInt(val) break case "data": data = val break } } // Do some error checking. if (rows <= 0) throw new IOException("Missing or invalid rows") if (cols <= 0) throw new IOException("Missing or invalid cols") if (data == null || !data.startsWith("<") || !data.endsWith(">")) { throw new IOException("Missing or invalid data") } if (cols * rows != data.length() - 2) { throw new IOException("Expected data length${cols * rows} but was ${data.length() - 2}") } // Build the resultant table. Table table = new DefaultGenericTable(cols, rows) int index = 1 for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { table.set(c, r, data.charAt(index++)) } } // HACK: Work around an SJJK bug when column headers are unspecified. for (int c = 0; c < cols; c++) table.setColumnHeader(c, "") return table } // HACK: Work around weird bug in Groovy(?). // It is normally not needed to override this method here. @Override public Class<String> getType() { return String.class } } // Register the plugin with the existing SciJava context. import org.scijava.plugin.PluginInfo info = new PluginInfo(CharTableReader.class, IOPlugin.class) ij.plugin().addPlugin(info) info  Out[3]: priority=0.0, enabled=true, pluginType=IOPlugin Now that we have an IOPlugin registered to handle the reading of .char-table files, let's give it a spin on the example data we wrote earlier: In [4]: table = ij.io().open(tablePath) ij.notebook().display((Object) table)  Out[4]: @@@@@@@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @@@ ## Creating a data writer¶ Similarly, IOPlugins also extend ImageJ's capabilities when writing data to external sources. In the future, ImageJ will have a unified File ▶ Save As... command which offers all available export options in a unified UI. But for the moment, the routine must be called programmatically via the IOService. Let's write an exporter for tables to another custom file format: .ascii-table. This format writes each table cell as a single readable ASCII character (32 - 126 inclusive); characters outside this range are written as - (45). All columns of a row are written on the same line, with a newline between each row. Of course, this can be a lossy export. Here is the exporter implementation for .ascii-table: In [5]: import java.io.File import org.scijava.Priority import org.scijava.io.AbstractIOPlugin import org.scijava.io.IOPlugin import org.scijava.plugin.Plugin import org.scijava.table.DefaultGenericTable import org.scijava.table.Table import org.scijava.util.DigestUtils import org.scijava.util.FileUtils @Plugin(type = IOPlugin.class) public class AsciiTableWriter extends AbstractIOPlugin<Table> { @Override public Class<Table> getDataType() { // This is the type of object exported by the writer. return Table.class } @Override public boolean supportsSave(final String destination) { // Check whether the destination should be a .ascii-table. // This is typically a format extension check. return destination.toLowerCase().endsWith(".ascii-table") } @Override public void save(final Table table, final String destination) throws IOException { // This is where we write the data to its destination, // converting it from the source data type. // Define the default character. byte other = (byte) '-' // Build up the output bytes. int cols = table.getColumnCount() int rows = table.getRowCount() byte[] bytes = new byte[(cols + 1) * rows] int index = 0 for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { Object cell = table.get(c, r) String s = cell == null ? null : cell.toString() if (s == null || s.length() == 0) { bytes[index++] = other continue } int v = s.charAt(0) bytes[index++] = v >= 32 && v <= 126 ? (byte) v : other } bytes[index++] = '\n' } // Write out the file. FileUtils.writeFile(new File(destination), bytes) } // HACK: Work around weird bug in Groovy(?). // It is normally not needed to override this method here. @Override public Class<String> getType() { return String.class } } // Register the plugin with the existing SciJava context. import org.scijava.plugin.PluginInfo info = new PluginInfo(AsciiTableWriter.class, IOPlugin.class) ij.plugin().addPlugin(info) // HACK: Refresh the IOService. (This bug is fixed on scijava-common master.) import org.scijava.util.ClassUtils ClassUtils.setValue(ClassUtils.getField(org.scijava.plugin.AbstractSingletonService.class, "instances"), ij.io(), null) info  Out[5]: priority=0.0, enabled=true, pluginType=IOPlugin In [6]: outPath = desktop + "fiji.ascii-table" ij.io().save(table, desktop + "fiji.ascii-table") outFile = new File(outPath) "Wrote${outFile.length()} bytes to '\$outFile'"

Out[6]:
Wrote 56 bytes to '/Users/curtis/Desktop/fiji.ascii-table'

Check that it did what we wanted:

In [7]:
import org.scijava.util.DigestUtils
import org.scijava.util.FileUtils
DigestUtils.string(FileUtils.readFile(outFile))

Out[7]:
@@@@@@@
@
@ @ @ @
@ @ @ @
@ @ @ @
@   @
@ @@@


## Supporting both reading and writing¶

If you wish to support both reading and writing to/from the same format, you can include both open and save implementations in the same IOPlugin.