This notebook demonstrates how to write an IOPlugin
, which handles reading and/or writing of external data to and from Java data structures.
%classpath config resolver imagej.public https://maven.imagej.net/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: imagej.public
ImageJ v2.0.0-rc-71 is ready to go.
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 countrows
to define the row countdata
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:
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'"
Wrote 78 bytes to '/Users/curtis/Desktop/example.char-table'
Without further ado, here is the IOPlugin
implementation:
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
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:
table = ij.io().open(tablePath)
ij.notebook().display((Object) table)
@ | @ | @ | @ | @ | @ | @ |
@ | ||||||
@ | @ | @ | @ | |||
@ | @ | @ | @ | |||
@ | @ | @ | @ | |||
@ | @ | |||||
@ | @ | @ | @ |
Similarly, IOPlugin
s 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
:
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
priority=0.0, enabled=true, pluginType=IOPlugin
outPath = desktop + "fiji.ascii-table"
ij.io().save(table, desktop + "fiji.ascii-table")
outFile = new File(outPath)
"Wrote ${outFile.length()} bytes to '$outFile'"
Wrote 56 bytes to '/Users/curtis/Desktop/fiji.ascii-table'
Check that it did what we wanted:
import org.scijava.util.DigestUtils
import org.scijava.util.FileUtils
DigestUtils.string(FileUtils.readFile(outFile))
@@@@@@@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @@@
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
.