{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Extending ImageJ: Data I/O" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook demonstrates how to write an `IOPlugin`, which handles reading and/or writing of external data to and from Java data structures." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Added new repo: imagej.public\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "", "version_major": 2, "version_minor": 0 }, "method": "display_data" }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "24c4dd1c-8a8f-4835-8a01-cccbfdedd37a", "version_major": 2, "version_minor": 0 }, "method": "display_data" }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "ImageJ v2.0.0-rc-71 is ready to go." ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%classpath config resolver imagej.public https://maven.imagej.net/content/groups/public\n", "%classpath add mvn net.imagej imagej 2.0.0-rc-71\n", "ij = new net.imagej.ImageJ()\n", "\"ImageJ v${ij.getVersion()} is ready to go.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating a data reader" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. \n", "\n", "The format of a `.char-table` file is key-value pairs, one per line. Valid keys are:\n", "\n", "* `cols` to define the column count\n", "* `rows` to define the row count\n", "* `data` to specify the actual character data: a sequence of $cols \\times rows$ characters enumerated in row-major order, and bracketed by `<` and `>`.\n", "\n", "Let's start by writing out an example `.char-table` file, which we will use for testing:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Wrote 78 bytes to '/Users/curtis/Desktop/example.char-table'" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import java.io.File\n", "import org.scijava.util.DigestUtils\n", "import org.scijava.util.FileUtils\n", "data = \"\"\"\n", "cols = 7\n", "rows = 7\n", "data = <@@@@@@@@ @ @ @ @@ @ @ @@ @ @ @@ @ @ @@@ >\n", "\"\"\"\n", "desktop = System.getProperty(\"user.home\") + \"/Desktop/\"\n", "tablePath = desktop + \"example.char-table\"\n", "outFile = new File(tablePath)\n", "FileUtils.writeFile(outFile, DigestUtils.bytes(data))\n", "\"Wrote ${outFile.length()} bytes to '$outFile'\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Without further ado, here is the `IOPlugin` implementation:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "priority=0.0, enabled=true, pluginType=IOPlugin" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import java.io.File\n", "import org.scijava.io.AbstractIOPlugin\n", "import org.scijava.io.IOPlugin\n", "import org.scijava.plugin.Plugin\n", "import org.scijava.table.DefaultGenericTable\n", "import org.scijava.table.Table\n", "import org.scijava.util.DigestUtils\n", "import org.scijava.util.FileUtils\n", "\n", "@Plugin(type = IOPlugin.class)\n", "public class CharTableReader extends AbstractIOPlugin {\n", "\n", " @Override\n", " public Class
getDataType() {\n", " // This is the type of object produced by the reader.\n", " return Table.class\n", " }\n", "\n", " @Override\n", " public boolean supportsOpen(final String source) {\n", " // Check whether the source is indeed a .char-table.\n", " // This check can be as shallow or as deep as you want,\n", " // but it is advised to keep it as fast as possible.\n", " // As such, it is not recommended to actually open and\n", " // interrogate the source unless you have no choice.\n", " return source.toLowerCase().endsWith(\".char-table\")\n", " }\n", "\n", " @Override\n", " public Table open(final String source) throws IOException {\n", " // This is where we read the data from its source,\n", " // and convert it into the destination data type.\n", "\n", " // Read in the file.\n", " String contents = DigestUtils.string(FileUtils.readFile(new File(source)))\n", "\n", " // Parse the contents line by line.\n", " int rows = 0, cols = 0\n", " String data = null\n", " for (line in contents.split(\"\\n\")) {\n", " int equals = line.indexOf(\"=\")\n", " if (equals < 0) continue\n", " String key = line.substring(0, equals).trim()\n", " String val = line.substring(equals + 1).trim()\n", " switch (key) {\n", " case \"rows\":\n", " rows = Integer.parseInt(val)\n", " break\n", " case \"cols\":\n", " cols = Integer.parseInt(val)\n", " break\n", " case \"data\":\n", " data = val\n", " break\n", " } \n", " }\n", "\n", " // Do some error checking.\n", " if (rows <= 0) throw new IOException(\"Missing or invalid rows\")\n", " if (cols <= 0) throw new IOException(\"Missing or invalid cols\")\n", " if (data == null || !data.startsWith(\"<\") || !data.endsWith(\">\")) {\n", " throw new IOException(\"Missing or invalid data\")\n", " }\n", " if (cols * rows != data.length() - 2) {\n", " throw new IOException(\"Expected data length ${cols * rows} but was ${data.length() - 2}\")\n", " }\n", "\n", " // Build the resultant table.\n", " Table table = new DefaultGenericTable(cols, rows)\n", " int index = 1\n", " for (int r = 0; r < rows; r++) {\n", " for (int c = 0; c < cols; c++) {\n", " table.set(c, r, data.charAt(index++))\n", " }\n", " }\n", "\n", " // HACK: Work around an SJJK bug when column headers are unspecified.\n", " for (int c = 0; c < cols; c++) table.setColumnHeader(c, \"\")\n", "\n", " return table\n", " }\n", "\n", " // HACK: Work around weird bug in Groovy(?).\n", " // It is normally not needed to override this method here.\n", " @Override\n", " public Class getType() { return String.class }\n", "}\n", "\n", "// Register the plugin with the existing SciJava context.\n", "import org.scijava.plugin.PluginInfo\n", "info = new PluginInfo(CharTableReader.class, IOPlugin.class)\n", "ij.plugin().addPlugin(info)\n", "info" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
@@@@@@@
@
@ @ @ @
@ @ @ @
@ @ @ @
@ @
@ @@@
" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "table = ij.io().open(tablePath)\n", "ij.notebook().display((Object) table)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating a data writer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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`.\n", "\n", "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.\n", "\n", "Here is the exporter implementation for `.ascii-table`:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "priority=0.0, enabled=true, pluginType=IOPlugin" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import java.io.File\n", "import org.scijava.Priority\n", "import org.scijava.io.AbstractIOPlugin\n", "import org.scijava.io.IOPlugin\n", "import org.scijava.plugin.Plugin\n", "import org.scijava.table.DefaultGenericTable\n", "import org.scijava.table.Table\n", "import org.scijava.util.DigestUtils\n", "import org.scijava.util.FileUtils\n", "\n", "@Plugin(type = IOPlugin.class)\n", "public class AsciiTableWriter extends AbstractIOPlugin {\n", "\n", " @Override\n", " public Class
getDataType() {\n", " // This is the type of object exported by the writer.\n", " return Table.class\n", " }\n", "\n", " @Override\n", " public boolean supportsSave(final String destination) {\n", " // Check whether the destination should be a .ascii-table.\n", " // This is typically a format extension check.\n", " return destination.toLowerCase().endsWith(\".ascii-table\")\n", " }\n", "\n", " @Override\n", " public void save(final Table table, final String destination) throws IOException {\n", " // This is where we write the data to its destination,\n", " // converting it from the source data type.\n", "\n", " // Define the default character.\n", " byte other = (byte) '-'\n", "\n", " // Build up the output bytes.\n", " int cols = table.getColumnCount()\n", " int rows = table.getRowCount()\n", " byte[] bytes = new byte[(cols + 1) * rows]\n", " int index = 0\n", " for (int r = 0; r < rows; r++) {\n", " for (int c = 0; c < cols; c++) {\n", " Object cell = table.get(c, r)\n", " String s = cell == null ? null : cell.toString()\n", " if (s == null || s.length() == 0) {\n", " bytes[index++] = other\n", " continue\n", " }\n", " int v = s.charAt(0)\n", " bytes[index++] = v >= 32 && v <= 126 ? (byte) v : other\n", " }\n", " bytes[index++] = '\\n'\n", " }\n", "\n", " // Write out the file.\n", " FileUtils.writeFile(new File(destination), bytes)\n", " }\n", "\n", " // HACK: Work around weird bug in Groovy(?).\n", " // It is normally not needed to override this method here.\n", " @Override\n", " public Class getType() { return String.class }\n", "}\n", "\n", "// Register the plugin with the existing SciJava context.\n", "import org.scijava.plugin.PluginInfo\n", "info = new PluginInfo(AsciiTableWriter.class, IOPlugin.class)\n", "ij.plugin().addPlugin(info)\n", "\n", "// HACK: Refresh the IOService. (This bug is fixed on scijava-common master.)\n", "import org.scijava.util.ClassUtils\n", "ClassUtils.setValue(ClassUtils.getField(org.scijava.plugin.AbstractSingletonService.class, \"instances\"), ij.io(), null)\n", "\n", "info" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Wrote 56 bytes to '/Users/curtis/Desktop/fiji.ascii-table'" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "outPath = desktop + \"fiji.ascii-table\"\n", "ij.io().save(table, desktop + \"fiji.ascii-table\")\n", "outFile = new File(outPath)\n", "\"Wrote ${outFile.length()} bytes to '$outFile'\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Check that it did what we wanted:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "@@@@@@@\n", "@ \n", "@ @ @ @\n", "@ @ @ @\n", "@ @ @ @\n", "@ @ \n", "@ @@@ \n" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import org.scijava.util.DigestUtils\n", "import org.scijava.util.FileUtils\n", "DigestUtils.string(FileUtils.readFile(outFile))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Supporting both reading and writing\n", "\n", "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`." ] } ], "metadata": { "kernelspec": { "display_name": "Groovy", "language": "groovy", "name": "groovy" }, "language_info": { "codemirror_mode": "groovy", "file_extension": ".groovy", "mimetype": "", "name": "Groovy", "nbconverter_exporter": "", "version": "2.4.3" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "307px" }, "toc_section_display": true, "toc_window_display": true } }, "nbformat": 4, "nbformat_minor": 2 }